|
42791
|
1565
|
17
|
2026-05-14T12:00:14.107866+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760014107_m1.jpg...
|
PhpStorm
|
faVsco.js – Activity.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...
|
[{"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}]...
|
-3349951234057899453
|
-7193090647943312192
|
click
|
hybrid
|
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
SlackFileEditViewDMsActivityLaterMoreJiminny ...# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...•- Direct messages®. Galya DimitrovaAneliya Angelova•. Vasil VasilevStefka Stoyanova&: Todor StamatovMario GeorgievNikolay Ivanov2o James Graham. Stoyan TanevSteliyan GeorgievPetko KashinskiLukas Kovalik y...::: AppsToastJira CloudConala CalaGoHistoryWindowHelp>0.(ahlSupport Daily - now→Describe what you are looking forVasil VasilevSupport Dailynow - 15:00-15:15= Notes - Support Daily.MessagesAdd canvas@ Files& Pins +ще пиша на Галя да я питам какво да правимVasil Vasilev 2:42 PMмоето е по скоро идеяв момента не го правим, но и почти не го показваме никьде като информацияние имаме някаква история на сделката, имаме стейдж по време на импорта на активититоно тоя стейдж го ползваме само за това търсене в on demand (edited)преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insightsимаме дата на отваряне и затваряне на сделказащо да не сложим и един маркер кога се е променил стейджатака в таймлайна ще се вижда "развитието" на тая сделкаLukas Kovalik 2:45 PMхм ами то дали да го няма вечеVasil Vasilev 2:45 PMняма гоLukas Kovalik 2:46 PMняма дано ВЕ си го връщас тази цел точноVasil Vasilev 2:47 PMзнам, проверих, преди да предложа на Галя да ги покажемВЪЗМОЖНОно явно не е направеноMessage Vasil Vasilev+100% C8• Thu 14 May 15:00:13C Join Google Meet...
|
42790
|
NULL
|
NULL
|
NULL
|
|
42792
|
1566
|
13
|
2026-05-14T12:00:14.113485+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760014113_m2.jpg...
|
PhpStorm
|
faVsco.js – Activity.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...
|
[{"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}]...
|
8963127460541569662
|
-7051499938828275774
|
click
|
hybrid
|
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
PhostormFV faVsco.jsProject= env.local=.env.migrate= env.nikilocal=.env.other=.env.productionE.env.production-euE.env.qaE.env.qaiE.env.rootE .env.stagingE.gitattributes•.gitignore=,ono-cs-rixer.cachephp.ono-cs-rixer.dist.ono=.ohounit.result.cacheE prettierianore= windsurtrulesphp ide heloer.onophp ide heloer modes.ondphp artisanM. CLAUDE mdcomooser.isoncomooser.lockdenendencv-checker.ison8 dev isonl=ide tytE infoction ison distMLINSTALI mdMLINTEPNAI WERHOOK SETUD mdEjiminny_storageMiliconcos mdlM Makefile# package-lock.jsonEphostan.neon.distE phostan-baseline.neon< phpunit.xmlTe raw sal querv.sallM+ README.mdsô sonar-project.properties= testou‹> Untitled Diagram.ymllus vetur. contio.isMI WEBHOOK FILTERING IMPLEMENTih External LibrariesE° Scratches and Consolesv Database ConsolesVASU#concole =llliADEAI PISKS IEUTA DIISUnA EU TELNPushed JY-20904-fix-update-es-on-activity-conVIewINavicareCodeLaravelKeractorWindow°9 JY-20904-fix-update-es-on-activity-coyUserinvitationdro.onp= custom.logscratch &.ison= laravel.logA SF jiminny@localhost]& HS_local [jiminny@localhost]& console [PROD]C EmailTextRelay.pnp© Sso.php© ValidateSendingMessage.phpA console (EU] Xiii accounts [EU]fii stages (EU]tid teams [EU]ImporbotkecoraingJoo.onpx© Activity.php© DashboardController.php© ActivityRepository.php© FixActivitiesOpportunity.phg& console SlAGiNG© ConferenceCrmMatcherJob.php)Playbookereated.pngc createPlaybookcreatedevent.png© EventserviceProvider.pnpSo jiminnyso mivle wawozar pulund wwrss© FieldRepository.php© ProspectCache.php© StaleRecordValidator.php030 A9 A27 V.3 V 106 ^©OpportunityRepository.php© UpdateSingleEntity.phpC) MatchActivityCrmData.php(c) Service.php© Client.phe1642SELECT * FROM USErS WHERE name LIKE '%Ne1L HoyLex"; # 17651MELE * FROM social accounts WhERE socIabLe 10 = 176515OpportunitySynctrait.pho(C) PayloadBuilder.php(c) Contiquration.phpm 81 л v 1644SELE * FROM activities WHERE uuid to bind 975c6830-7049-4cle-b2e9-ac80c10a738a') = uuid:>use...19 Gclass ImportActivityuypes impLements Shouldqueue* Create the event Zistener.public function __construct(private readonly ResolveTeamCrmConnection ScrmResolver,private readonly FieldRepository $fieldRepository.private readonly PlaybookCategoryRepository Srepository,1...}1645 VSELECT * FROM opportunities WHERE id IN (7842553, 6211727):8.164.SELE * FROM contacts WHERE 1d IN 10202124. 62117279411641SELECT * FROM opportunity_stages WHERE opportunity_id = 7842553;16481649CONCAT(u.id, CASE WHEN V.id = t.owner_id THEN ' (owner)' ELSE'' END) AS user_id,165016511652u. emaillsa.*t.owner_id FROM social_accounts saJOIN users u on u.id = sa.sociable_idJOIN teams t 1..n<->1: on t.id = u.team_idVHERE u.team_id = 519 and sa.provider = 'hubspot';* Import the standard Event/Task Type picklist options from the CRMpublic function handle(PlaybookCreated $event): voidSplaybook = Sevent->playbook:// Don't run if somehow we already have categoriesif (Splaybook->qetCategories(->isNotEmptyO) {Svalues = 11:tryScrmService = sthis->crmResolver->resolveForTeam(Solavbook->aetTeamon:ScrmService->svncFieldsolavbook->aetActivitv=ield00:Svalues = $crmService->importPicklistValues(Splaybook->getActivityFieldO):} catch (Throwable Se) {Loa. •warninad messaae'[ImportActivityTypes) CRM API failed, falling back to database values', ["nlavbook id!= Snlavhook->aetTdol.'team_id' => $playbook->getTeamIdO,=> $e->getMessageO.if (empty(Svalues)) {Svalues = Sthis->fetchActivityFieldValues(Splaybook->qetActivityFieldO):165416551ACA 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 * from stages where team_id = 519: # 1877818//5GELECTcrm provider 1d.stage_idstade undated at.undated atROM onnontunitiesVHERE id IN (6211727, 7842553);SELECT * FROM opportunity_contactsWHEPE onnontunitv id = 4211727 AND contact id - 10202724•SELECT id, name, stage id, is closed, is won, updated at, remotely created at=ROM opportunitiesMHEPE Account id - 91701341pn5p RV undatod a+ nESC.| 11supoont Dally • now100% L2• Thu 14 May 15:00:13AskJiminnyReportActivityServiceTest vPipedrive SDK EvaluaFixing Activity Stage DImport CRM Activity T+0 ..I've implemented Fix 4 in two locations:1. Activitv::updateActivitvCrmdata• lines 2122-2126a oho.if (Sopportunity d& $this->opportunity_id !== Sopportunity→>id) ‹sches→opporcuniy1o = sopporcunetyo,Reset stage id when opportunity changes to avoid stale stage from previous opportunity matchSthis->stage_id = $stage?->id;Potential Issue: If Sstage is null, this will set stage id to null even it the new opportunity has a stage in the databaseThis could happen if the CRM lookup doesn't return stage informationRecommendation: Consider checkina if the opportunity has a stage id and usina that as a falllbackO phpif Sonnortunitv ss Sthis-sonnortunitv id l== Sonnortunitv-sidhSthis->opportunity id = sopportunity->id:to avoid stale stage from previous opportunity matchsthis->stage_id = $stage?->id ?? Sopportunity->stage_id;However, looking at the context, $stage is derived from the CRM lookup results, so if it's null, the opportunity might nothave stage intormation avallable. The current behavior is acceptable.2. FixActivities0pportunitv command (lines 123-128)oho'opportunity_id' => $opportunity->getId(),'stage 1d = sopportunitv->getstage.donNo iccues found - This correctly undates hoth fielde toaetherAthor Iosstinne roviowod.•IntegrationApp/Service.php (line 560): Sets stage id directiv from opportunitvesstage id when creating/updatingfrom lead data - this is correct.• Copy command: Copies both opportunity id and stage id together - this is correct.• ActivityController: Handles stage id from user requests - not related to the bugTest ResultsUodateCrbataTect : 2 tests passed (6 assertions)• MatchCrm0bjectTest : 15 tests passed (75 assertions)The implementation is sound. The only minor concern is the null stage case in updateActivityCrmData(), but this isaccontablo aivon tha contovtfnl .Your included weekly usage quota is exhausted. Purchase extra usage to continue using premium models. Quota resets May 17, 11:00Ask anvthina (*4L)es CodoSWE.1GNN Windeurf Toame 166041 UITC.9Aensodw...
|
42789
|
NULL
|
NULL
|
NULL
|
|
42793
|
1565
|
18
|
2026-05-14T12:00:16.046467+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760016046_m1.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewDMsActivityLaterMoreJiminny ...# SlackFileEditViewDMsActivityLaterMoreJiminny ...# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...•- Direct messages®. Galya DimitrovaAneliya Angelova•. Vasil VasilevStefka Stoyanova&: Todor StamatovMario GeorgievNikolay Ivanov2o James Graham. Stoyan TanevSteliyan GeorgievPetko KashinskiLukas Kovalik y...::: AppsToastJira CloudConala CalaGoHistoryWindowHelp>0.(ahlSupport Daily - now→Describe what you are looking forVasil VasilevSupport Dailynow - 15:00-15:15= Notes - Support Daily.MessagesAdd canvas@ Files& Pins +ще пиша на Галя да я питам какво да правимVasil Vasilev 2:42 PMмоето е по скоро идеяв момента не го правим, но и почти не го показваме никьде като информацияние имаме някаква история на сделката, имаме стейдж по време на импорта на активититоно тоя стейдж го ползваме само за това търсене в on demand (edited)преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insightsимаме дата на отваряне и затваряне на сделказащо да не сложим и един маркер кога се е променил стейджатака в таймлайна ще се вижда "развитието" на тая сделкаLukas Kovalik 2:45 PMхм ами то дали да го няма вечеVasil Vasilev 2:45 PMняма гоLukas Kovalik 2:46 PMняма дано ВЕ си го връщас тази цел точноVasil Vasilev 2:47 PMзнам, проверих, преди да предложа на Галя да ги покажемВЪЗМОЖНОно явно не е направеноMessage Vasil Vasilev+100% C8• Thu 14 May 15:00:15C Join Google Meet...
|
NULL
|
98455502234825243
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewDMsActivityLaterMoreJiminny ...# SlackFileEditViewDMsActivityLaterMoreJiminny ...# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...•- Direct messages®. Galya DimitrovaAneliya Angelova•. Vasil VasilevStefka Stoyanova&: Todor StamatovMario GeorgievNikolay Ivanov2o James Graham. Stoyan TanevSteliyan GeorgievPetko KashinskiLukas Kovalik y...::: AppsToastJira CloudConala CalaGoHistoryWindowHelp>0.(ahlSupport Daily - now→Describe what you are looking forVasil VasilevSupport Dailynow - 15:00-15:15= Notes - Support Daily.MessagesAdd canvas@ Files& Pins +ще пиша на Галя да я питам какво да правимVasil Vasilev 2:42 PMмоето е по скоро идеяв момента не го правим, но и почти не го показваме никьде като информацияние имаме някаква история на сделката, имаме стейдж по време на импорта на активититоно тоя стейдж го ползваме само за това търсене в on demand (edited)преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insightsимаме дата на отваряне и затваряне на сделказащо да не сложим и един маркер кога се е променил стейджатака в таймлайна ще се вижда "развитието" на тая сделкаLukas Kovalik 2:45 PMхм ами то дали да го няма вечеVasil Vasilev 2:45 PMняма гоLukas Kovalik 2:46 PMняма дано ВЕ си го връщас тази цел точноVasil Vasilev 2:47 PMзнам, проверих, преди да предложа на Галя да ги покажемВЪЗМОЖНОно явно не е направеноMessage Vasil Vasilev+100% C8• Thu 14 May 15:00:15C Join Google Meet...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
42794
|
1565
|
19
|
2026-05-14T12:00:20.266312+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760020266_m1.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.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
Code changed:
Hide
Sync Changes
Hide This Notification
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
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-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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\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}]...
|
-2958445655377132052
|
7218327201310389032
|
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
Code changed:
Hide
Sync Changes
Hide This Notification
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
42793
|
NULL
|
NULL
|
NULL
|
|
42795
|
1566
|
14
|
2026-05-14T12:00:20.276839+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760020276_m2.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.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
Code changed:
Hide
Sync Changes
Hide This Notification
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
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-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":"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":"1","depth":4,"bounds":{"left":0.38464096,"top":0.22426178,"width":0.00731383,"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\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"bounds":{"left":0.122340426,"top":0.16759777,"width":0.2869016,"height":0.8324022},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"2","depth":4,"bounds":{"left":0.7140958,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7237367,"top":0.123703115,"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.123703115,"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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"bounds":{"left":0.42985374,"top":0.0622506,"width":0.33178192,"height":0.9377494},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\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}]...
|
-2958445655377132052
|
7218327201310389032
|
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
Code changed:
Hide
Sync Changes
Hide This Notification
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
42796
|
1566
|
15
|
2026-05-14T12:00:22.129296+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760022129_m2.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.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
Code changed:
Hide
Sync Changes
Hide This Notification
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options...
|
[{"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":"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":"1","depth":4,"bounds":{"left":0.38464096,"top":0.22426178,"width":0.00731383,"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\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"bounds":{"left":0.122340426,"top":0.16759777,"width":0.2869016,"height":0.8324022},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"2","depth":4,"bounds":{"left":0.7140958,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7237367,"top":0.123703115,"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.123703115,"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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"bounds":{"left":0.42985374,"top":0.0622506,"width":0.33178192,"height":0.9377494},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update(['opportunity_id' => $opportunity->getId()]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\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}]...
|
3491498159251776389
|
7218327201310389032
|
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
Code changed:
Hide
Sync Changes
Hide This Notification
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update(['opportunity_id' => $opportunity->getId()]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options...
|
42795
|
NULL
|
NULL
|
NULL
|
|
42797
|
1565
|
20
|
2026-05-14T12:00:31.830515+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760031830_m1.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.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
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-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":"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}]...
|
2413002196346074587
|
-7048967763515953210
|
click
|
hybrid
|
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
Code changed:
Hide
Sync Changes
SlackFileEditViewDMsActivityLaterMoreJiminny ...# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...•- Direct messages®. Galya DimitrovaAneliya Angelova•. Vasil VasilevStefka Stoyanova&: Todor StamatovMario GeorgievNikolay Ivanov2o James Graham. Stoyan TanevSteliyan GeorgievPetko KashinskiLukas Kovalik y...::: AppsToastJira CloudConala CalaGoHistoryWindowHelp>0.(ahlSupport Daily - now→Describe what you are looking forVasil VasilevSupport Dailynow - 15:00-15:15= Notes - Support Daily.MessagesAdd canvas@ Files& Pins +ще пиша на Галя да я питам какво да правимVasil Vasilev 2:42 PMмоето е по скоро идеяв момента не го правим, но и почти не го показваме никьде като информацияние имаме някаква история на сделката, имаме стейдж по време на импорта на активититоно тоя стейдж го ползваме само за това търсене в on demand (edited)преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insightsимаме дата на отваряне и затваряне на сделказащо да не сложим и един маркер кога се е променил стейджатака в таймлайна ще се вижда "развитието" на тая сделкаLukas Kovalik 2:45 PMхм ами то дали да го няма вечеVasil Vasilev 2:45 PMняма гоLukas Kovalik 2:46 PMняма дано ВЕ си го връщас тази цел точноVasil Vasilev 2:47 PMзнам, проверих, преди да предложа на Галя да ги покажемВЪЗМОЖНОно явно не е направеноMessage Vasil Vasilev+100% C8• Thu 14 May 15:00:31C Join Google Meet...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
42798
|
1566
|
16
|
2026-05-14T12:00:31.830536+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760031830_m2.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.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...
|
[{"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}]...
|
8963127460541569662
|
-7051499938828275774
|
click
|
hybrid
|
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
PhostormINavicarecodeFV faVsco.jsProiect vC) DeleteForCoachecComDownloaamissing lrackc rixAcuivitiesopportunit©) HaraDeleteActivitiesto© HardDeleteActivitiesTec) hyoratecalwincrmba© HydrateDefaultActivitv1C) InviteMeetinaBot.php© IterateActivities.phd© MiarateLocationFromC:© MonitorDialerActivities(C) MonitorMeetingcounte© MonitorMeetingEndCorC) MonitorMeetinostartco(t) MonitorMeetinaTrait.orC) NotifvNotLodded.oho© NotPlayableEmail.phpC) PreMeetinaNotification© PreMeetingReminder.plc) ProheMediaSeamentsel© ReassignTranscriptConC) ReindeyRecentActivitie(c) RetrvProspectsummary(C) SetProviderCanabilitiec(e) StatusCount.php© SyncMissingCallDisposi© UpdateActivityElasticS€© UpdateElasticSearch.pl•_ Analvucsw Calendarsyalerml> C Hubspot› IntearationApp> D Traits© AddLavoutEntities.php© AutoloaDelavedCommaC) Backfilll@pportunitvUseiC BullhornCommandAbstC) BullhornPinaCommand.C) BullhornSearchCommarC) BullhornSessioncomma@ CheckActivitvl.cagableC) CleanDunlicateSieldDatC) 1oaActivitiesCommand@ ManageSvncStrategyC@ MatchOnnortunituAativ(C MiaratoDrovidor nhn(e) DrococcHubenntOhiont(e) DuracDelotodOnnortuniched.IV.2090/.fixcundate.ecconcactivitvcd© TextRelayService.phg© ValidateSendingMessage.php© ActivityRepository.php© EventserviceProvider.pnp© StaleRecordValidator.phpC) MatchActivityCrmData.php(c) Service.php© Client.pheC) OpportunityRepository.php© UpdateSingleEntity.phpOpportunitySyncTrait.pho(C) PayloadBuilder.phpmA1 . v19 04class ImportActzvityuypes implements shouldQueue* Create the event Zistener.public function __construct(private readonly ResolveTeamCrmConnection ScrmResolver,private readonly FieldRepository $fieldRepository.private readonly PlaybookCategoryRepository Srepository,1...}* Import the standard Event/Task Type picklist options from the CRMpublic function handle(PlaybookCreated $event): voidSplaybook = Sevent->[URL_WITH_CREDENTIALS] HS_local [jiminny@localhost]& console [PROD]A console (EU]in accounts cu)fii stages (EU]tid teams [EU]ImporbotkecoraingJoo.pnp© Activity.php(C) SixActivitiesOnnortunitv.nhn Xi& console SlAGiNGnamespace Jaminny console commanas Acclvicles•use ...181class FixActivitiesOpportunity extends Command20 0protected ssignature = 'activity:f1x-opportunity {--from=} 1--to=}';public function handle(EmailHelper SemailHelper): intf...}1usadeprivate function aetCrmService(Team Stean): ServiceIntenface!... }1 usaaelorivate function processParticinantsdServicelnterface ScrmService,Emal Helner SemailHelner.Activity Cactivitv): void iSteam = Sactivity->getTeamSuser = Sactivity->getUserO›Sparticipants = Sactivity->getParticipantsO:foreach (Sparticipants as $participant)if (Sparticipant->getUserId !== null || $participant->getEmailAddress === null) {continue.Sema1LAddress = Sparticipant->qetzma1lAddressO*if (SemailHelper->isCompanyEmail(Steam, SemailAddress)) {continue:try s110Sopportunitv = $this->find0pportunitvInCrm($crmService. SemailAddness. Suser->aetId@):catch (Throwable Se) 4Sthis->error( string: 'Could not find opportunity:' . $e->getMessageO):Sopportunity = nulli* Sonnontunitv === nul){iSthic->npsetActivitvlnnontunitv/Sactivitv)•continue:supoont Dally • now100% L2• Thu 14 May 15:00:31U AskJiminnyReportActivityServiceTest vPipedrive SDK EvaluaFixing Activity Stage DImport CRM Activity T+0 ..I've implemented Fix 4 in two locations:1. Activitv::updateActivitvCrmdata (lines 2122-2126).whohoif (Sopportunity d& $this->opportunity_id !== Sopportunity→>id) ‹sches→opporcuniy1o = sopporcunetyo,Reset stage id when opportunity changes to avoid stale stage from previous opportunity matchSthis->stage_id = $stage?->id;Potential Issue: If Sstage is null, this will set stage id to null even it the new opportunity has a stage in the databaseThis could happen if the CRM lookup doesn't return stage informationRecommendation: Consider checkina if the opportunity has a stage id and usina that as a fallbackO phpif Sonnortunitv ss Sthis-sonnortunitv id l== Sonnortunitv-sid)Sthis->opportunity id = sopportunity->id:to avoid stale stage from previous opportunity matchsthis->stage_id = $stage?->id ?? Sopportunity->stage_id;However, looking at the context, $stage is derived from the CRM lookup results, so if it's null, the opportunity might nothave stage intormation avallable. The current behavior is acceptable2. FixActivities0pportunitv command (lines 123-128)oho'opportunity_id' => $opportunity->getId(),'stage 1d = sopportunitv->getstage.donNo iccues found - This correctly undates hoth fielde toaetherOther locations reviewed:•IntearationApp/Service.php (line 560: Sets stage id directly from opportunity→stage id when creatina/uodatinafrom lead data - this is correct.• Copy command: Copies both opportunity id and stage id together - this is correct.• ActivityController: Handles stage id from user requests - not related to the bugTest Results• UodateCrmbataTect : 2 tests passed (6 assertions)• MatchCrm0bjectTest : 15 tests passed (75 assertions)The imolementation is sound. The only minor concern is the null stage case in updateActivitvCrmDatao, but this isaccontablo aivon tha contovtfnl .• Your included weekly usage quota is exhausted. Purchase extra usage to continue using premium models. Quota resets May 17, 11:00Lets havl÷ @Code SWF-1.6W Windsurf Toams 18.21/24 charc UTF.8Po 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
42799
|
1565
|
21
|
2026-05-14T12:00:33.536925+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760033536_m1.jpg...
|
PhpStorm
|
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Update Project...
Commit…
Push…
New Branch…
Checko Update Project...
Commit…
Push…
New Branch…
Checkout Tag or Revision…
Recent
JY-20904-fix-update-es-on-activity-command
master
JY-20903-update_activity-stage-on-opportunity-change
JY-20891-improve-sms-text-relays
JY-20725-handle-HS-search-rate-limit
JY-20904-fix-update-es-on-activity-command
master
JY-20903-update_activity-stage-on-opportunity-change
JY-20891-improve-sms-text-relays
JY-20725-handle-HS-search-rate-limit
Local
Remote
Tags
Search
Fetch
Settings...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Update Project...","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Commit…","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Push…","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"New Branch…","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Checkout Tag or Revision…","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Recent","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"JY-20904-fix-update-es-on-activity-command","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"master","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"JY-20903-update_activity-stage-on-opportunity-change","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"JY-20891-improve-sms-text-relays","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"JY-20725-handle-HS-search-rate-limit","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"JY-20904-fix-update-es-on-activity-command","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"master","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"JY-20903-update_activity-stage-on-opportunity-change","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"JY-20891-improve-sms-text-relays","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"JY-20725-handle-HS-search-rate-limit","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Local","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Remote","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Tags","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"Search","depth":1,"on_screen":true,"help_text":"Search for branches and actions","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Fetch","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-2549539283952137956
|
8566018148934453326
|
click
|
accessibility
|
NULL
|
Update Project...
Commit…
Push…
New Branch…
Checko Update Project...
Commit…
Push…
New Branch…
Checkout Tag or Revision…
Recent
JY-20904-fix-update-es-on-activity-command
master
JY-20903-update_activity-stage-on-opportunity-change
JY-20891-improve-sms-text-relays
JY-20725-handle-HS-search-rate-limit
JY-20904-fix-update-es-on-activity-command
master
JY-20903-update_activity-stage-on-opportunity-change
JY-20891-improve-sms-text-relays
JY-20725-handle-HS-search-rate-limit
Local
Remote
Tags
Search
Fetch
Settings...
|
42797
|
NULL
|
NULL
|
NULL
|
|
42801
|
1566
|
17
|
2026-05-14T12:00:34.982338+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760034982_m2.jpg...
|
PhpStorm
|
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Checkout
Delete
Checkout and Rebase onto 'JY-2 Checkout
Delete
Checkout and Rebase onto 'JY-20904-fix-update-es-on-activity-command'
Checkout and Update
Compare with 'JY-20904-fix-update-es-on-activity-command'
Show Diff with Working Tree
Rebase 'JY-20904-fix-update-es-on-activity-command' onto 'JY-20903-update_activity-stage-on…hange'
Merge 'JY-20903-update_activity-stage-on…hange' into 'JY-20904-fix-update-es-on-activity-command'
New Worktree from 'JY-20903-updateactivity-stage-on-opportunity-change'…...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Checkout","depth":5,"bounds":{"left":0.27027926,"top":1.0,"width":0.13264628,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Delete","depth":5,"bounds":{"left":0.27027926,"top":1.0,"width":0.13264628,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Checkout and Rebase onto 'JY-20904-fix-update-es-on-activity-command'","depth":5,"bounds":{"left":0.27027926,"top":1.0,"width":0.13264628,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Checkout and Update","depth":5,"bounds":{"left":0.27027926,"top":1.0,"width":0.13264628,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Compare with 'JY-20904-fix-update-es-on-activity-command'","depth":5,"bounds":{"left":0.27027926,"top":1.0,"width":0.13264628,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Show Diff with Working Tree","depth":5,"bounds":{"left":0.27027926,"top":1.0,"width":0.13264628,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Rebase 'JY-20904-fix-update-es-on-activity-command' onto 'JY-20903-update_activity-stage-on…hange'","depth":5,"bounds":{"left":0.27027926,"top":1.0,"width":0.13264628,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Merge 'JY-20903-update_activity-stage-on…hange' into 'JY-20904-fix-update-es-on-activity-command'","depth":5,"bounds":{"left":0.27027926,"top":1.0,"width":0.13264628,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"New Worktree from 'JY-20903-updateactivity-stage-on-opportunity-change'…","depth":5,"bounds":{"left":0.27027926,"top":1.0,"width":0.13264628,"height":0.0},"on_screen":false,"role_description":"text"}]...
|
-2652013696331504585
|
680360319164506827
|
click
|
accessibility
|
NULL
|
Checkout
Delete
Checkout and Rebase onto 'JY-2 Checkout
Delete
Checkout and Rebase onto 'JY-20904-fix-update-es-on-activity-command'
Checkout and Update
Compare with 'JY-20904-fix-update-es-on-activity-command'
Show Diff with Working Tree
Rebase 'JY-20904-fix-update-es-on-activity-command' onto 'JY-20903-update_activity-stage-on…hange'
Merge 'JY-20903-update_activity-stage-on…hange' into 'JY-20904-fix-update-es-on-activity-command'
New Worktree from 'JY-20903-updateactivity-stage-on-opportunity-change'…...
|
42798
|
NULL
|
NULL
|
NULL
|
|
42800
|
1565
|
22
|
2026-05-14T12:00:34.997732+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760034997_m1.jpg...
|
PhpStorm
|
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Checkout
New Branch from 'JY-20903-update_acti Checkout
New Branch from 'JY-20903-update_activity-stage-on…hange'…
Checkout and Rebase onto 'JY-20904-fix-update-es-on-activity-command'
Checkout and Update
Compare with 'JY-20904-fix-update-es-on-activity-command'
Show Diff with Working Tree
Delete
Merge 'JY-20903-update_activity-stage-on…hange' into 'JY-20904-fix-update-es-on-activity-command'...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Checkout","depth":5,"bounds":{"left":0.0,"top":0.0,"width":0.27708334,"height":0.024444444},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"New Branch from 'JY-20903-update_activity-stage-on…hange'…","depth":5,"bounds":{"left":0.0,"top":0.0,"width":0.27708334,"height":0.024444444},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Checkout and Rebase onto 'JY-20904-fix-update-es-on-activity-command'","depth":5,"bounds":{"left":0.0,"top":0.0,"width":0.27708334,"height":0.024444444},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Checkout and Update","depth":5,"bounds":{"left":0.0,"top":0.0,"width":0.27708334,"height":0.024444444},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Compare with 'JY-20904-fix-update-es-on-activity-command'","depth":5,"bounds":{"left":0.0,"top":0.0,"width":0.27708334,"height":0.024444444},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Show Diff with Working Tree","depth":5,"bounds":{"left":0.0,"top":0.0,"width":0.27708334,"height":0.024444444},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Delete","depth":5,"bounds":{"left":0.0,"top":0.0,"width":0.27708334,"height":0.024444444},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Merge 'JY-20903-update_activity-stage-on…hange' into 'JY-20904-fix-update-es-on-activity-command'","depth":5,"bounds":{"left":0.0,"top":0.0,"width":0.27708334,"height":0.024444444},"on_screen":false,"role_description":"text"}]...
|
5330416083942683891
|
716503422380156107
|
click
|
accessibility
|
NULL
|
Checkout
New Branch from 'JY-20903-update_acti Checkout
New Branch from 'JY-20903-update_activity-stage-on…hange'…
Checkout and Rebase onto 'JY-20904-fix-update-es-on-activity-command'
Checkout and Update
Compare with 'JY-20904-fix-update-es-on-activity-command'
Show Diff with Working Tree
Delete
Merge 'JY-20903-update_activity-stage-on…hange' into 'JY-20904-fix-update-es-on-activity-command'...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
42802
|
1566
|
18
|
2026-05-14T12:00:36.698903+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760036698_m2.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20903-update_activity- Project: faVsco.js, menu
JY-20903-update_activity-stage-on-opportunity-change, 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-20903-update_activity-stage-on-opportunity-change, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12932181,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20903-update_activity-stage-on-opportunity-change","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}]...
|
2309948694457577418
|
-8629521083764847645
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20903-update_activity- Project: faVsco.js, menu
JY-20903-update_activity-stage-on-opportunity-change, menu
Start Listening for PHP Debug Connections
PhostormINavicarecodeFV faVsco.js°9 JY-20904-fix-update-es-orProiect vC) DeleteForCoachecComDownloaamissing lrackc rixAcuivitiesopportunitC) HaraDeleteActivitiestol© HardDeleteActivitiesTec) hyoratecalwincrmba© HydrateDefaultActivitv1C) InviteMeetinaBot.php© IterateActivities.phd© MiarateLocationFromC:© MonitorDialerActivities(C) MonitorMeetingcounte© MonitorMeetingEndCorC) MonitorMeetinostartco(t) MonitorMeetinaTrait.orC) NotifvNotLodded.oho© NotPlayableEmail.phpC) PreMeetinaNotification© PreMeetingReminder.plc) ProheMediaSeamentsel© ReassignTranscriptConC) ReindeyRecentActivitie(c) RetrvProspectsummary(C) SetProviderCanabilitiec(e) StatusCount.php© SyncMissingCallDisposi© UpdateActivityElasticS€© UpdateElasticSearch.pl•_ Analvucsw Calendars> C Hubspot› IntearationApp> C Traits© AddLavoutEntities.php© AutoloaDelavedCommaC) Backfilll@pportunitvUseiC BullhornCommandAbstC) BullhornPinaCommand.C) BullhornSearchCommarC) BullhornSessioncomma@ CheckActivitvl.cagableC) CleanDunlicateSieldDatC) 1oaActivitiesCommand@ ManageSvncStrategyC@ MatchOnnortunituAativ(C MiaratoDrovidor nhn© ProcessHubspotObject(e) DuracDelotodOnnortuniched.IV.2090/.fixcundate.ecconcactivitvcd© TextRelayService.php© ValidateSendingMessage.php© ActivityRepository.php© EventserviceProvider.pnp© StaleRecordValidator.phpC) MatchActivityCrmData.php(c) Service.php© Client.pheC) OpportunityRepository.php© UpdateSingleEntity.phpOpportunitySyncTrait.pho(C) PayloadBuilder.phpmA1л v19 04class ImportActzvityuypes implements shouldQueue* Create the event Zistener.public function __construct(private readonly ResolveTeamCrmConnection ScrmResolver,private readonly FieldRepository $fieldRepository.private readonly PlaybookCategoryRepository Srepository,1...}* Import the standard Event/Task Type picklist options from the CRMpublic function handle(PlaybookCreated $event): voidSplaybook = Sevent->[URL_WITH_CREDENTIALS] HS_local [jiminny@localhost]& console [PROD]A console (EU]in accounts cu)fii stages (EU]tid teams [EU]ImporbotkecoraingJoo.pnp© Activity.php(C) SixActivitiesOnnortunitv.nhn Xi& console SlAGiNGnamespace Jaminny console commanas Acclvicles•use ...181class FixActivitiesOpportunity extends Command20 0protected ssignature = 'activity:f1x-opportunity {--from=} 1--to=}';public function handle(EmailHelper SemailHelper): intf...}1usadeprivate function aetCrmService(Team Stean): ServiceIntenface!... }1 usaaelorivate function processParticinantsdServicelnterface ScrmService,Emal Helner SemailHelner.Activity Cactivitv): void iSteam = Sactivity->getTeamSuser = Sactivity->getUserO›Sparticipants = Sactivity->getParticipantsO:foreach (Sparticipants as $participant)if (Sparticipant->getUserId !== null || $participant->getEmailAddress === null) {continue.Sema1LAddress = Sparticipant->qetzma1lAddressO*if (SemailHelper->isCompanyEmail(Steam, SemailAddress)) {continue:try s110Sopportunitv = $this->find0pportunitvInCrm($crmService. SemailAddness. Suser->aetId@):catch (Throwable Se) 4Sthis->error( string: 'Could not find opportunity:' . $e->getMessageO):Sopportunity = nulli* Sonnontunitv === nul){iSthic->npsetActivitvlnnontunitv/Sactivitv)•continue:supoont Dally • now100% S2• Thu 14 May 15:00:36U AskJiminnyReportActivityServiceTest vPipedrive SDK EvaluaFixing Activity Stage DImport CRM Activity T+0 ..I've implemented Fix 4 in two locations:1. Activitv::updateActivitvCrmdata (lines 2122-2126).whohoif (Sopportunity d& $this->opportunity_id !== Sopportunity→>id) ‹sches→opporcuniy1o = sopporcunetyo,Reset stage id when opportunity changes to avoid stale stage from previous opportunity matchSthis->stage_id = $stage?->id;Potential Issue: If Sstage is null, this will set stage id to null even it the new opportunity has a stage in the databaseThis could happen if the CRM lookup doesn't return stage informationRecommendation: Consider checkina if the opportunity has a stage id and usina that as a fallbackO phpif Sonnortunitv ss Sthis-sonnortunitv id l== Sonnortunitv-sid)Sthis->opportunity id = sopportunity->id:to avoid stale stage from previous opportunity matchsthis->stage_id = $stage?->id ?? Sopportunity->stage_id;However, looking at the context, $stage is derived from the CRM lookup results, so if it's null, the opportunity might nothave stage intormation avallable. The current behavior is acceptable2. FixActivities0pportunitv command (lines 123-128)oho'opportunity_id' => $opportunity->getId(),'stage 1d = sopportunitv->getstage.donNo iccues found - This correctly undates hoth fielde toaetherOther locations reviewed:•IntearationApp/Service.php (line 560: Sets stage id directly from opportunity→stage id when creatina/uodatinafrom lead data - this is correct.• Copy command: Copies both opportunity id and stage id together - this is correct.• ActivityController: Handles stage id from user requests - not related to the bugTest Results• UodateCrmbataTect : 2 tests passed (6 assertions)• MatchCrm0bjectTest : 15 tests passed (75 assertions)The imolementation is sound. The only minor concern is the null stage case in updateActivitvCrmDatao, but this isaccontablo aivon tha contovtfnl .• Your included weekly usage quota is exhausted. Purchase extra usage to continue using premium models. Quota resets May 17, 11:00Lets hav÷ @Code SWF-1.6W Windsurf Toams 18.21/24 charc UTF.8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
42804
|
1566
|
19
|
2026-05-14T12:00:43.415663+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760043415_m2.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Checked out JY-20903-update_activity-stage-on-oppo Checked out JY-20903-update_activity-stage-on-opportunity-change
text/html
text/html
text/html
text/html
Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXTextField","text [{"role":"AXTextField","text":"Checked out JY-20903-update_activity-stage-on-opportunity-change","depth":3,"bounds":{"left":0.016954787,"top":0.858739,"width":0.1549202,"height":0.016759777},"on_screen":true,"value":"Checked out JY-20903-update_activity-stage-on-opportunity-change","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"bounds":{"left":0.016954787,"top":0.8603352,"width":0.027260639,"height":0.013567438},"on_screen":true,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"bounds":{"left":0.045877658,"top":0.8603352,"width":0.12765957,"height":0.013567438},"on_screen":true,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"1","depth":4,"bounds":{"left":0.38464096,"top":0.22426178,"width":0.00731383,"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\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"bounds":{"left":0.122340426,"top":0.16759777,"width":0.2869016,"height":0.8324022},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"2","depth":4,"bounds":{"left":0.7140958,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7237367,"top":0.123703115,"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.123703115,"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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"bounds":{"left":0.42985374,"top":0.07581804,"width":0.33178192,"height":0.92418194},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\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}]...
|
-5862623290099894891
|
7216075401488839464
|
click
|
accessibility
|
NULL
|
Checked out JY-20903-update_activity-stage-on-oppo Checked out JY-20903-update_activity-stage-on-opportunity-change
text/html
text/html
text/html
text/html
Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
42802
|
NULL
|
NULL
|
NULL
|
|
42803
|
1565
|
23
|
2026-05-14T12:00:43.426899+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760043426_m1.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Checked out JY-20903-update_activity-stage-on-oppo Checked out JY-20903-update_activity-stage-on-opportunity-change
text/html
text/html
text/html
text/html
Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXTextField","text [{"role":"AXTextField","text":"Checked out JY-20903-update_activity-stage-on-opportunity-change","depth":3,"on_screen":true,"value":"Checked out JY-20903-update_activity-stage-on-opportunity-change","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":true,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":true,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\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}]...
|
-5862623290099894891
|
7216075401488839464
|
click
|
accessibility
|
NULL
|
Checked out JY-20903-update_activity-stage-on-oppo Checked out JY-20903-update_activity-stage-on-opportunity-change
text/html
text/html
text/html
text/html
Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
42800
|
NULL
|
NULL
|
NULL
|
|
42805
|
NULL
|
0
|
2026-05-14T12:01:13.769625+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760073769_m1.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\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}]...
|
-7818674109799218818
|
7216084197590250280
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
42806
|
NULL
|
0
|
2026-05-14T12:01:13.796279+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760073796_m2.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"1","depth":4,"bounds":{"left":0.38464096,"top":0.22426178,"width":0.00731383,"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\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"bounds":{"left":0.122340426,"top":0.16759777,"width":0.2869016,"height":0.8324022},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"2","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"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.73138297,"top":0.123703115,"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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"bounds":{"left":0.42985374,"top":0.07581804,"width":0.33178192,"height":0.92418194},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\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}]...
|
-7818674109799218818
|
7216084197590250280
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
42807
|
1567
|
0
|
2026-05-14T12:01:44.089277+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760104089_m1.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\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}]...
|
-7818674109799218818
|
7216084197590250280
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
42805
|
NULL
|
NULL
|
NULL
|
|
42808
|
1568
|
0
|
2026-05-14T12:01:44.170374+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760104170_m2.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"1","depth":4,"bounds":{"left":0.38464096,"top":0.22426178,"width":0.00731383,"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\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"bounds":{"left":0.122340426,"top":0.16759777,"width":0.2869016,"height":0.8324022},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"2","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"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.73138297,"top":0.123703115,"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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"bounds":{"left":0.42985374,"top":0.07581804,"width":0.33178192,"height":0.92418194},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\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}]...
|
-7818674109799218818
|
7216084197590250280
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
42806
|
NULL
|
NULL
|
NULL
|
|
42809
|
1567
|
1
|
2026-05-14T12:02:14.433840+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760134433_m1.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\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}]...
|
-7818674109799218818
|
7216084197590250280
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
42805
|
NULL
|
NULL
|
NULL
|
|
42810
|
1568
|
1
|
2026-05-14T12:02:14.513884+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760134513_m2.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"1","depth":4,"bounds":{"left":0.38464096,"top":0.22426178,"width":0.00731383,"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\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"bounds":{"left":0.122340426,"top":0.16759777,"width":0.2869016,"height":0.8324022},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"2","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"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.73138297,"top":0.123703115,"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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"bounds":{"left":0.42985374,"top":0.07581804,"width":0.33178192,"height":0.92418194},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\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}]...
|
-7818674109799218818
|
7216084197590250280
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
42806
|
NULL
|
NULL
|
NULL
|
|
42811
|
1567
|
2
|
2026-05-14T12:02:44.786727+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760164786_m1.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\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}]...
|
-7818674109799218818
|
7216084197590250280
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
42805
|
NULL
|
NULL
|
NULL
|
|
42812
|
1568
|
2
|
2026-05-14T12:02:45.010923+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760165010_m2.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"1","depth":4,"bounds":{"left":0.38464096,"top":0.22426178,"width":0.00731383,"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\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"bounds":{"left":0.122340426,"top":0.16759777,"width":0.2869016,"height":0.8324022},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"2","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"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.73138297,"top":0.123703115,"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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"bounds":{"left":0.42985374,"top":0.07581804,"width":0.33178192,"height":0.92418194},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\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}]...
|
-7818674109799218818
|
7216084197590250280
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
42806
|
NULL
|
NULL
|
NULL
|
|
42813
|
1567
|
3
|
2026-05-14T12:03:15.121556+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760195121_m1.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\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}]...
|
-7818674109799218818
|
7216084197590250280
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
42805
|
NULL
|
NULL
|
NULL
|
|
42814
|
1568
|
3
|
2026-05-14T12:03:15.388166+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760195388_m2.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"1","depth":4,"bounds":{"left":0.38464096,"top":0.22426178,"width":0.00731383,"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\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"bounds":{"left":0.122340426,"top":0.16759777,"width":0.2869016,"height":0.8324022},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"2","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"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.73138297,"top":0.123703115,"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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"bounds":{"left":0.42985374,"top":0.07581804,"width":0.33178192,"height":0.92418194},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\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}]...
|
-7818674109799218818
|
7216084197590250280
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
42806
|
NULL
|
NULL
|
NULL
|
|
42815
|
1567
|
4
|
2026-05-14T12:03:45.418249+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760225418_m1.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\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}]...
|
-7818674109799218818
|
7216084197590250280
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
42805
|
NULL
|
NULL
|
NULL
|
|
42816
|
1568
|
4
|
2026-05-14T12:03:45.759636+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760225759_m2.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"1","depth":4,"bounds":{"left":0.38464096,"top":0.22426178,"width":0.00731383,"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\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"bounds":{"left":0.122340426,"top":0.16759777,"width":0.2869016,"height":0.8324022},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"2","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"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.73138297,"top":0.123703115,"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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"bounds":{"left":0.42985374,"top":0.07581804,"width":0.33178192,"height":0.92418194},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\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}]...
|
-7818674109799218818
|
7216084197590250280
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
42806
|
NULL
|
NULL
|
NULL
|
|
42817
|
1568
|
5
|
2026-05-14T12:04:16.133060+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760256133_m2.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"1","depth":4,"bounds":{"left":0.38464096,"top":0.22426178,"width":0.00731383,"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\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"bounds":{"left":0.122340426,"top":0.16759777,"width":0.2869016,"height":0.8324022},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"2","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"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.73138297,"top":0.123703115,"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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"bounds":{"left":0.42985374,"top":0.07581804,"width":0.33178192,"height":0.92418194},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\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}]...
|
-7818674109799218818
|
7216084197590250280
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
42806
|
NULL
|
NULL
|
NULL
|
|
42818
|
1567
|
5
|
2026-05-14T12:04:42.489060+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760282489_m1.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\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}]...
|
-7818674109799218818
|
7216084197590250280
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
42805
|
NULL
|
NULL
|
NULL
|
|
42819
|
1568
|
6
|
2026-05-14T12:04:46.546498+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760286546_m2.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"1","depth":4,"bounds":{"left":0.38464096,"top":0.22426178,"width":0.00731383,"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\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"bounds":{"left":0.122340426,"top":0.16759777,"width":0.2869016,"height":0.8324022},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"2","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"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.73138297,"top":0.123703115,"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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"bounds":{"left":0.42985374,"top":0.07581804,"width":0.33178192,"height":0.92418194},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\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}]...
|
-7818674109799218818
|
7216084197590250280
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
42806
|
NULL
|
NULL
|
NULL
|
|
42820
|
1567
|
6
|
2026-05-14T12:05:12.790779+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760312790_m1.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\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}]...
|
-7818674109799218818
|
7216084197590250280
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
42805
|
NULL
|
NULL
|
NULL
|
|
42821
|
1568
|
7
|
2026-05-14T12:05:16.912801+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760316912_m2.jpg...
|
PhpStorm
|
faVsco.js – FixActivitiesOpportunity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"1","depth":4,"bounds":{"left":0.38464096,"top":0.22426178,"width":0.00731383,"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\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"bounds":{"left":0.122340426,"top":0.16759777,"width":0.2869016,"height":0.8324022},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"2","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"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.73138297,"top":0.123703115,"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\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\n }\n}","depth":4,"bounds":{"left":0.42985374,"top":0.07581804,"width":0.33178192,"height":0.92418194},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Console\\Commands\\Activities;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Jiminny\\Console\\Commands\\Command;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Throwable;\n\nclass FixActivitiesOpportunity extends Command\n{\n protected $signature = 'activity:fix-opportunity {--from=} {--to=}';\n\n public function handle(EmailHelper $emailHelper): int\n {\n $from = $this->option('from');\n $to = $this->option('to');\n\n $validator = Validator::make(\n ['from' => $from, 'to' => $to],\n [\n 'from' => ['required', 'date'],\n 'to' => ['required', 'date'],\n ]\n );\n\n if ($validator->fails()) {\n $this->error('Validation failed:');\n $this->output->block($validator->errors()->all());\n\n return Command::FAILURE;\n }\n\n $this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);\n\n $activityIds = Activity::select('activities.id')\n ->whereBetween('activities.created_at', [$from, $to])\n ->join('users', 'activities.user_id', '=', 'users.id')\n ->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')\n ->whereColumn('opportunities.team_id', '<>', 'users.team_id')\n ->pluck('id');\n\n $this->info('Found activities: ' . $activityIds->count());\n\n foreach ($activityIds as $activityId) {\n $activity = Activity::where('id', $activityId)\n ->with(['user', 'participants'])\n ->first();\n\n $crmService = $this->getCrmService($activity->getTeam());\n if ($crmService === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $this->processParticipants($crmService, $emailHelper, $activity);\n }\n\n return Command::SUCCESS;\n }\n\n private function getCrmService(Team $team): ?ServiceInterface\n {\n $config = $team->getCrmConfiguration();\n\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $team->getOwner(),\n 'providerSlug' => $config->getProviderName(),\n ]);\n\n try {\n return $crmResolver->prepareCrmService();\n } catch (Throwable $e) {\n $this->error('Could not prepare CRM service: ' . $e->getMessage());\n\n return null;\n }\n }\n\n private function processParticipants(\n ServiceInterface $crmService,\n EmailHelper $emailHelper,\n Activity $activity\n ): void {\n $team = $activity->getTeam();\n $user = $activity->getUser();\n $participants = $activity->getParticipants();\n\n foreach ($participants as $participant) {\n if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {\n continue;\n }\n\n $emailAddress = $participant->getEmailAddress();\n if ($emailHelper->isCompanyEmail($team, $emailAddress)) {\n continue;\n }\n\n try {\n $opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());\n } catch (Throwable $e) {\n $this->error('Could not find opportunity: ' . $e->getMessage());\n\n $opportunity = null;\n }\n\n if ($opportunity === null) {\n $this->resetActivityOpportunity($activity);\n\n continue;\n }\n\n $activity->update([\n 'opportunity_id' => $opportunity->getId(),\n 'stage_id' => $opportunity->getStageId(),\n ]);\n\n $this->info('Opportunity updated for activity: ' . $activity->getId());\n }\n }\n\n private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity\n {\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $match = $decorator->matchExactlyByEmail($emailAddress, $userId);\n\n if (empty($match)) {\n $match = $decorator->matchByDomain($emailAddress, $userId);\n }\n\n if (empty($match)) {\n return null;\n }\n\n [, , $opportunity, ,] = $match;\n\n return $opportunity;\n }\n\n private function resetActivityOpportunity(Activity $activity): void\n {\n $this->info('Reset opportunity for activity: ' . $activity->getId());\n $activity->update(['opportunity_id' => null]);\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}]...
|
-7818674109799218818
|
7216084197590250280
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Console\Commands\Activities;
use Illuminate\Support\Facades\Validator;
use Jiminny\Console\Commands\Command;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Models\Activity;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Team;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Throwable;
class FixActivitiesOpportunity extends Command
{
protected $signature = 'activity:fix-opportunity {--from=} {--to=}';
public function handle(EmailHelper $emailHelper): int
{
$from = $this->option('from');
$to = $this->option('to');
$validator = Validator::make(
['from' => $from, 'to' => $to],
[
'from' => ['required', 'date'],
'to' => ['required', 'date'],
]
);
if ($validator->fails()) {
$this->error('Validation failed:');
$this->output->block($validator->errors()->all());
return Command::FAILURE;
}
$this->info('Fixing opportunity for activities from ' . $from . ' to ' . $to);
$activityIds = Activity::select('activities.id')
->whereBetween('activities.created_at', [$from, $to])
->join('users', 'activities.user_id', '=', 'users.id')
->join('opportunities', 'activities.opportunity_id', '=', 'opportunities.id')
->whereColumn('opportunities.team_id', '<>', 'users.team_id')
->pluck('id');
$this->info('Found activities: ' . $activityIds->count());
foreach ($activityIds as $activityId) {
$activity = Activity::where('id', $activityId)
->with(['user', 'participants'])
->first();
$crmService = $this->getCrmService($activity->getTeam());
if ($crmService === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$this->processParticipants($crmService, $emailHelper, $activity);
}
return Command::SUCCESS;
}
private function getCrmService(Team $team): ?ServiceInterface
{
$config = $team->getCrmConfiguration();
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $team->getOwner(),
'providerSlug' => $config->getProviderName(),
]);
try {
return $crmResolver->prepareCrmService();
} catch (Throwable $e) {
$this->error('Could not prepare CRM service: ' . $e->getMessage());
return null;
}
}
private function processParticipants(
ServiceInterface $crmService,
EmailHelper $emailHelper,
Activity $activity
): void {
$team = $activity->getTeam();
$user = $activity->getUser();
$participants = $activity->getParticipants();
foreach ($participants as $participant) {
if ($participant->getUserId() !== null || $participant->getEmailAddress() === null) {
continue;
}
$emailAddress = $participant->getEmailAddress();
if ($emailHelper->isCompanyEmail($team, $emailAddress)) {
continue;
}
try {
$opportunity = $this->findOpportunityInCrm($crmService, $emailAddress, $user->getId());
} catch (Throwable $e) {
$this->error('Could not find opportunity: ' . $e->getMessage());
$opportunity = null;
}
if ($opportunity === null) {
$this->resetActivityOpportunity($activity);
continue;
}
$activity->update([
'opportunity_id' => $opportunity->getId(),
'stage_id' => $opportunity->getStageId(),
]);
$this->info('Opportunity updated for activity: ' . $activity->getId());
}
}
private function findOpportunityInCrm(ServiceInterface $crmService, string $emailAddress, int $userId): ?Opportunity
{
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$match = $decorator->matchExactlyByEmail($emailAddress, $userId);
if (empty($match)) {
$match = $decorator->matchByDomain($emailAddress, $userId);
}
if (empty($match)) {
return null;
}
[, , $opportunity, ,] = $match;
return $opportunity;
}
private function resetActivityOpportunity(Activity $activity): void
{
$this->info('Reset opportunity for activity: ' . $activity->getId());
$activity->update(['opportunity_id' => null]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
42806
|
NULL
|
NULL
|
NULL
|
|
42823
|
1568
|
8
|
2026-05-14T12:05:42.966860+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760342966_m2.jpg...
|
Slack
|
! Vasil Vasilev (DM) - Jiminny Inc - 6 new items - ! Vasil Vasilev (DM) - Jiminny Inc - 6 new items - Slack...
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
1
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
bugs
confusion-clinic
curiosity_lab
engineering
general
jiminny-bg
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Vasil Vasilev
Galya Dimitrova
Aneliya Angelova
Stefka Stoyanova
Todor Stamatov
Mario Georgiev
Nikolay Ivanov
James Graham
Stoyan Tanev
Steliyan Georgiev
Petko Kashinski
Lukas Kovalik
you...
|
[{"role":"AXPopUpButton","text [{"role":"AXPopUpButton","text":"Switch workspaces… (Jiminny Inc) Has new messages","depth":14,"bounds":{"left":0.27593085,"top":1.0,"width":0.011968086,"height":-0.058260202},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Home","depth":14,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Home","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"DMs","depth":14,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"DMs","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Activity","depth":14,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Activity","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":14,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Later","depth":14,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Later","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"More…","depth":14,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Unreads","depth":21,"bounds":{"left":0.30718085,"top":1.0,"width":0.018949468,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Threads","depth":21,"bounds":{"left":0.30718085,"top":1.0,"width":0.01761968,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Huddles","depth":21,"bounds":{"left":0.30718085,"top":1.0,"width":0.018284574,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drafts & sent","depth":21,"bounds":{"left":0.30718085,"top":1.0,"width":0.02925532,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":21,"bounds":{"left":0.3587101,"top":1.0,"width":0.0026595744,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Directories","depth":21,"bounds":{"left":0.30718085,"top":1.0,"width":0.024268618,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-x-integration-app","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.043882977,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"platform-inner-team","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.04454787,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ai-chapter","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.022273935,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"alerts","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.012300532,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"backend","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.018284574,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"bugs","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.010638298,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"confusion-clinic","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.034574468,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"curiosity_lab","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.027593086,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"engineering","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.025930852,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"general","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-bg","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"platform-tickets","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"product_launches","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"random","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"releases","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"sofia-office","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"support","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"thank-yous","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"the_people_of_jiminny","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Vasil Vasilev","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Galya Dimitrova","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Stefka Stoyanova","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Todor Stamatov","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Mario Georgiev","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"James Graham","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tanev","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Steliyan Georgiev","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Petko Kashinski","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Lukas Kovalik","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"you","depth":23,"on_screen":true,"role_description":"text"}]...
|
741772226420309527
|
-5828549904501490571
|
click
|
hybrid
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
1
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
bugs
confusion-clinic
curiosity_lab
engineering
general
jiminny-bg
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Vasil Vasilev
Galya Dimitrova
Aneliya Angelova
Stefka Stoyanova
Todor Stamatov
Mario Georgiev
Nikolay Ivanov
James Graham
Stoyan Tanev
Steliyan Georgiev
Petko Kashinski
Lukas Kovalik
you
PhostormINavicarecodeFV faVsco.js( #12077 on JY-20903-update_activity-stageProiect© TextRelayService.phpC) DeleteForCoachecCom© ValidateSendingMessage.phpDownloaamissing lrack© ActivityRepository.phpc rixAcuivitiesopportunitC) HaraDeleteActivitiestol© HardDeleteActivitiesTe© EventserviceProvider.pnp© ProspectCache.php© StaleRecordValidator.phpc) hyoratecalwincrmba© HydrateDefaultActivitv1C) OpportunityRepository.php© UpdateSingleEntity.phpC) MatchActivityCrmData.php(c) Service.php© Client.pheC) InviteMeetinaBot.php© IterateActivities.phd© MiarateLocationFromC:© MonitorDialerActivities(OpportunitySyncTrait.pho(C) PayloadBuilder.php(c) Confiquration.phpmA1л vC) MonitorMeetingcounte© MonitorMeetingEndCor19 04class ImportActzvityuypes implements shouldQueueC) MonitorMeetinostartco(t) MonitorMeetinaTrait.orC) NotifvNotLodded.oho© NotPlayableEmail.phpC) PreMeetinaNotification© PreMeetingReminder.plc) ProheMediaSeamentselC) RoaccianTranscrintComC) ReindeyRecentActivitie(c) RetrvProspectsummary(C) SetProviderCanabilitiec(e) StatusCount.php© SyncMissingCallDisposi© UpdateActivityElasticS€© UpdateElasticSearch.pl•_ Analvucsw Calendarsyalerml> C Hubspot› IntearationApp> C Traits© AddLavoutEntities.php© AutoloaDelavedCommaC) Backfilll@pportunitvUseiC BullhornCommandAbstC) BullhornPinaCommand.C) BullhornSearchCommarC) BullhornSessioncomma@ CheckActivitvl.cagableC) CleanDunlicateSieldDatC) 1oaActivitiesCommand@ ManageSvncStrategyC@ MatchOnnortunituAativ(C MiaratoDrovidor nhn© ProcessHubspotObject(e) DuracDelotodOnnortunChecked out IV.20902-unda* Create the event Listener..public function __construct(private readonly ResolveTeamCrmConnection ScrmResolver,private readonly FieldRepository $fieldRepository.private readonly PlaybookCategoryRepository Srepository,1...}* Import the standard Event/Task Type picklist options from the CRMpublic function handle(PlaybookCreated $event): voidSplaybook = Sevent->[URL_WITH_CREDENTIALS] HS_local [jiminny@localhost]& console [PROD]A console (EU]tiò accounts [EU]fii stages (EU]tid teams (EU]ImporbotkecoraingJoo.pnp© Activity.php(C) SixActivitiesOnnortunitv.nhn Xi& console SlAGiNGnamespace Jiminny console commanas Accivicles•use ...181class FixActivitiesOpportunity extends Command20 0protected ssignature = 'activity:f1x-opportunity {--from=} 1--to=}';public function handle(EmailHelper SemailHelper): int{...}1usadeorivate function detcrmservicecfeam steam: ?Serviceinterface.....1 usageorivate function processParticinantsdServicelnterface ScrmService,Emal Helner SemailHelner.Activity Cactivitv): void iSteam = Sactivity->getTeamSuser = Sactivity->getUserOSparticipants = Sactivity->getParticipantsO:foreach (Sparticipants as $participant)if (Sparticipant->getUserId !== null || $participant->getEmailAddress === null) {continue.Sema1LAddress = Sparticipant->qetzma1lAddressO*if (SemailHelper->isCompanyEmail(Steam, SemailAddress)) {continue:trysSopportunitv = $this->find0pportunitvInCrm($crmService. SemailAddress. Suser->qetId@):catch (Throwable Se) 4$this->error( string: 'Could not find opportunity:' . $e->getMessageO):Sopportunity = null.i* (Sonnontunitv === null){iSthis->npsetActivitvinnontunitv/Sactivitv)•continue:B2^supoont Dally • now100% L2• Thu 14 May 15:05:42AskJiminnyReportActivityServiceTest vPipedrive SDK EvaluaFixing Activity Stage DImport CRM Activity T+0 ..I've implemented Fix 4 in two locations:1. Activitv::updateActivitvCrmdata (lines 2122-2126).whohoif (Sopportunity && $this→>opportunity_id != Sopportunity→>id) €sches→opporcuniy1o = sopporcunetyo,Reset stage id when opportunity changes to avoid stale stage from previous opportunity matchSthis->stage_id = $stage?->id;Potential Issue: If Sstage is null, this will set stage id to null even it the new opportunity has a stage in the databaseThis could happen if the CRM lookup doesn't return stage informationRecommendation: Consider checkina if the opportunity has a stage id and usina that as a fallbackO phpif Sonnortunitv &s Sthis-sonnortunitv id l== Sonnortunitv-sidh «Sthis->opportunity id = sopportunity->id:to avoid stale stage from previous opportunity matchsthis->stage_id = $stage?->id ?? Sopportunity->stage_id;However, looking at the context, $stage is derived from the CRM lookup results, so if it's null, the opportunity might nothave stage intormation avallable. The current behavior is acceptable2. FixActivities0pportunitv command (lines 123-128)oho'opportunity_id' => $opportunity->getId(),'stage 1d = sopportunitv->getstage.donNo iccues found - This correctly undates hoth fielde toaetherOther locations reviewed:•IntearationApp/Service.php (line 560: Sets stage id directly from opportunity→stage id when creatina/uodatinafrom lead data - this is correct.• Copy command: Copies both opportunity id and stage id together - this is correct.• ActivityController: Handles stage id from user requests - not related to the bugTest Results• UodateCrmbataTect : 2 tests passed (6 assertions)• MatchCrm0bjectTest : 15 tests passed (75 assertions)The imolementation is sound. The only minor concern is the null stage case in updateActivitvCrmDatao, but this isaccontablo aivon tha contovtfnl .• Your included weekly usage quota is exhausted. Purchase extra usage to continue using premium models. Quota resets May 17, 11:00Lets havel÷ @Code SWF-1.6W Windsurf Toams 18.21/24 charc UTF.8io 4 spaces...
|
42806
|
NULL
|
NULL
|
NULL
|
|
42822
|
1567
|
7
|
2026-05-14T12:05:42.984271+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760342984_m1.jpg...
|
Slack
|
! Vasil Vasilev (DM) - Jiminny Inc - 6 new items - ! Vasil Vasilev (DM) - Jiminny Inc - 6 new items - Slack...
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
1
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
bugs
confusion-clinic
curiosity_lab
engineering
general
jiminny-bg
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Vasil Vasilev
Galya Dimitrova
Aneliya Angelova
Stefka Stoyanova
Todor Stamatov
Mario Georgiev
Nikolay Ivanov
James Graham
Stoyan Tanev
Steliyan Georgiev
Petko Kashinski
Lukas Kovalik
you
Toast
Jira Cloud...
|
[{"role":"AXPopUpButton","text [{"role":"AXPopUpButton","text":"Switch workspaces… (Jiminny Inc) Has new messages","depth":14,"bounds":{"left":0.011805556,"top":0.08111111,"width":0.025,"height":0.04},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Home","depth":14,"bounds":{"left":0.00625,"top":0.14,"width":0.036111113,"height":0.075555556},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Home","depth":16,"bounds":{"left":0.013888889,"top":0.19222222,"width":0.020833334,"height":0.015555556},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"DMs","depth":14,"bounds":{"left":0.00625,"top":0.21555555,"width":0.036111113,"height":0.075555556},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"DMs","depth":16,"bounds":{"left":0.015972223,"top":0.26777777,"width":0.016666668,"height":0.015555556},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Activity","depth":14,"bounds":{"left":0.00625,"top":0.2911111,"width":0.036111113,"height":0.075555556},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Activity","depth":16,"bounds":{"left":0.011111111,"top":0.34333333,"width":0.027083334,"height":0.015555556},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.011111111,"top":0.34333333,"width":0.0055555557,"height":0.015555556}},{"char_start":1,"char_count":7,"bounds":{"left":0.015972223,"top":0.34333333,"width":0.022222223,"height":0.015555556}}],"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":14,"bounds":{"left":0.00625,"top":0.36666667,"width":0.036111113,"height":0.075555556},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":16,"bounds":{"left":0.016666668,"top":0.4188889,"width":0.015972223,"height":0.015555556},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.016666668,"top":0.4188889,"width":0.004166667,"height":0.015555556}},{"char_start":1,"char_count":4,"bounds":{"left":0.020833334,"top":0.4188889,"width":0.011805556,"height":0.015555556}}],"role_description":"text"},{"role":"AXRadioButton","text":"Later","depth":14,"bounds":{"left":0.00625,"top":0.4422222,"width":0.036111113,"height":0.075555556},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Later","depth":16,"bounds":{"left":0.015277778,"top":0.49444443,"width":0.018055556,"height":0.015555556},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"More…","depth":14,"bounds":{"left":0.00625,"top":0.5177778,"width":0.036111113,"height":0.075555556},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More","depth":16,"bounds":{"left":0.015277778,"top":0.57,"width":0.01875,"height":0.015555556},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Unreads","depth":21,"bounds":{"left":0.077083334,"top":0.12777779,"width":0.039583333,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Threads","depth":21,"bounds":{"left":0.077083334,"top":0.12777779,"width":0.036805555,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Huddles","depth":21,"bounds":{"left":0.077083334,"top":0.12777779,"width":0.038194444,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drafts & sent","depth":21,"bounds":{"left":0.077083334,"top":0.12777779,"width":0.06111111,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":21,"bounds":{"left":0.18472221,"top":0.12777779,"width":0.0055555557,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Directories","depth":21,"bounds":{"left":0.077083334,"top":0.12777779,"width":0.050694443,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-x-integration-app","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.09166667,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"platform-inner-team","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.093055554,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ai-chapter","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.046527777,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"alerts","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.025694445,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"backend","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.038194444,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"bugs","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.022222223,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"confusion-clinic","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.072222225,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"curiosity_lab","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.057638887,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"engineering","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.054166667,"height":0.012222222},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"general","depth":23,"bounds":{"left":0.088194445,"top":0.15111111,"width":0.034027778,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-bg","depth":23,"bounds":{"left":0.088194445,"top":0.18222222,"width":0.048611112,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"platform-tickets","depth":23,"bounds":{"left":0.088194445,"top":0.21333334,"width":0.072916664,"height":0.02},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.088194445,"top":0.21333334,"width":0.00625,"height":0.02}},{"char_start":1,"char_count":15,"bounds":{"left":0.094444446,"top":0.21333334,"width":0.06666667,"height":0.02}}],"role_description":"text"},{"role":"AXStaticText","text":"product_launches","depth":23,"bounds":{"left":0.088194445,"top":0.24444444,"width":0.08055556,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"random","depth":23,"bounds":{"left":0.088194445,"top":0.27555555,"width":0.035416666,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"releases","depth":23,"bounds":{"left":0.088194445,"top":0.30666667,"width":0.038194444,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"sofia-office","depth":23,"bounds":{"left":0.088194445,"top":0.33777776,"width":0.05138889,"height":0.02},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.088194445,"top":0.33777776,"width":0.0048611113,"height":0.02}},{"char_start":1,"char_count":11,"bounds":{"left":0.093055554,"top":0.33777776,"width":0.045833334,"height":0.02}}],"role_description":"text"},{"role":"AXStaticText","text":"support","depth":23,"bounds":{"left":0.088194445,"top":0.36888888,"width":0.036111113,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"thank-yous","depth":23,"bounds":{"left":0.088194445,"top":0.4,"width":0.05138889,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"the_people_of_jiminny","depth":23,"bounds":{"left":0.088194445,"top":0.4311111,"width":0.094444446,"height":0.02},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.088194445,"top":0.4311111,"width":0.004166667,"height":0.02}},{"char_start":1,"char_count":20,"bounds":{"left":0.09236111,"top":0.4311111,"width":0.09861111,"height":0.02}}],"role_description":"text"},{"role":"AXStaticText","text":"Vasil Vasilev","depth":23,"bounds":{"left":0.088194445,"top":0.5044444,"width":0.057638887,"height":0.02},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.088194445,"top":0.5044444,"width":0.0069444445,"height":0.02}},{"char_start":1,"char_count":12,"bounds":{"left":0.09513889,"top":0.5044444,"width":0.050694443,"height":0.02}}],"role_description":"text"},{"role":"AXStaticText","text":"Galya Dimitrova","depth":23,"bounds":{"left":0.088194445,"top":0.53555554,"width":0.07361111,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"bounds":{"left":0.088194445,"top":0.56666666,"width":0.07847222,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Stefka Stoyanova","depth":23,"bounds":{"left":0.088194445,"top":0.5977778,"width":0.079166666,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Todor Stamatov","depth":23,"bounds":{"left":0.088194445,"top":0.6288889,"width":0.072222225,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Mario Georgiev","depth":23,"bounds":{"left":0.088194445,"top":0.66,"width":0.07152778,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":23,"bounds":{"left":0.088194445,"top":0.6911111,"width":0.06736111,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"James Graham","depth":23,"bounds":{"left":0.088194445,"top":0.7222222,"width":0.06666667,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tanev","depth":23,"bounds":{"left":0.088194445,"top":0.75333333,"width":0.060416665,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Steliyan Georgiev","depth":23,"bounds":{"left":0.088194445,"top":0.78444445,"width":0.07986111,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Petko Kashinski","depth":23,"bounds":{"left":0.088194445,"top":0.8155556,"width":0.072222225,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Lukas Kovalik","depth":23,"bounds":{"left":0.088194445,"top":0.8466667,"width":0.061805554,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"you","depth":23,"bounds":{"left":0.15555556,"top":0.8466667,"width":0.013194445,"height":0.02},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.15555556,"top":0.8466667,"width":0.0048611113,"height":0.02}},{"char_start":1,"char_count":2,"bounds":{"left":0.16041666,"top":0.8466667,"width":0.011805556,"height":0.02}}],"role_description":"text"},{"role":"AXStaticText","text":"Toast","depth":23,"bounds":{"left":0.088194445,"top":0.92,"width":0.025694445,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jira Cloud","depth":23,"bounds":{"left":0.088194445,"top":0.95111114,"width":0.045833334,"height":0.02},"on_screen":true,"role_description":"text"}]...
|
-579842235157092779
|
-5972665109758002060
|
click
|
hybrid
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
1
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
bugs
confusion-clinic
curiosity_lab
engineering
general
jiminny-bg
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Vasil Vasilev
Galya Dimitrova
Aneliya Angelova
Stefka Stoyanova
Todor Stamatov
Mario Georgiev
Nikolay Ivanov
James Graham
Stoyan Tanev
Steliyan Georgiev
Petko Kashinski
Lukas Kovalik
you
Toast
Jira Cloud
HomeDMSActivityFilesLaterMoreSlack>0.(alolSupport Daily - nowFileEditViewGoHistoryJiminny ...# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Direct messagesC. Vasil VasilevGalya DimitrovaR. Aneliya AngelovaStefka Stoyanova* Todor StamatovMario GeorgievNikolay Ivanov2o James Graham. Stoyan TanevSteliyan GeorgievPetko KashinskiLukas Kovalik y...:: AppsToastJira CloudConala CalaWindowHelpQDescribe what you are looking forVasil VasilevSupport Dailynow - 15:00-15:15= Notes - Support Daily.• Messages& Pins +Add canvasO Filesмоето е по скоро идеяv 1 new messageв момента не го правим, но и почти не го показваме никьде като информацияние имаме някаква история на сделката, имаме стейдж по време на импорта на активититоно тоя стейдж го ползваме само за това тьрсене в on demand (edited)преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insightsимаме дата на отваряне и затваряне на сделказащо да не сложим и един маркер кога се е променил стейджатака в таймлайна ще се вижда "развитието" на тая сделкаLukas Kovalik 2:45 PMхм ами то дали да го няма вечеVasil Vasilev 2:45 PMняма гоLukas Kovalik 2:46 PMняма дано ВЕ си го врьщас тази цел точноVasil Vasilev 2:47 PMзнам, проверих, преди да предложа на Галя да ги покажемВъзможноно явно не е направеноVasil Vasilev 3:04 PMв крайна сметка тоя метод Activity::updateActivityCrmData се вика само при подмяна на opportunity, нали така?Message Vasil Vasilev+100% C8• Thu 14 May 15:05:42C Join Google MeetNew...
|
42805
|
NULL
|
NULL
|
NULL
|
|
42824
|
1567
|
8
|
2026-05-14T12:05:44.319436+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760344319_m1.jpg...
|
Slack
|
Vasil Vasilev (DM) - Jiminny Inc - 3 new items - S Vasil Vasilev (DM) - Jiminny Inc - 3 new items - Slack...
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
1
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
bugs
confusion-clinic
curiosity_lab
engineering
general
jiminny-bg
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Vasil Vasilev
Galya Dimitrova
Aneliya Angelova
Stefka Stoyanova
Todor Stamatov
Mario Georgiev
Nikolay Ivanov
James Graham
Stoyan Tanev
Steliyan Georgiev
Petko Kashinski
Lukas Kovalik
you
Toast
Jira Cloud
Google Calendar
Messages
Messages
Add canvas
Add canvas
Files
Files
Pins
Pins
Add and Edit Channel Tabs
Canvas
List
Folder
Lukas Kovalik
Today at 2:33:54 PM
2:33 PM
ами мисля че трием lead и негов стаге тогава
Today at 2:33:59 PM
2:33
трябва да го проверя
Vasil Vasilev
Today at 2:35:14 PM
2:35 PM
наскоро рових по тоя код
Today at 2:35:33 PM
2:35
махаме lead id и подменяме stage id с това от сделката
Today at 2:35:48 PM
2:35
въпроса е дали е редно да го правим от историческа гледна точка
Lukas Kovalik
Today at 2:40:15 PM
2:40 PM
ами тука не знам, като цяло това му беше идея
Today at 2:40:29 PM
2:40
да знаеш по кое време е направено activity
Today at 2:40:47 PM
2:40
но щом вече го сменяваме при lead
Today at 2:41:51 PM
2:41
не е линеарно една сделка или lead която се развива във времето, сега си скаче напред назад както го мачнем
Today at 2:42:30 PM
2:42
ще пиша на Галя да я питам какво да правим
Vasil Vasilev
Today at 2:42:41 PM
2:42 PM
моето е по скоро идея
Today at 2:42:54 PM
2:42
в момента не го правим, но и почти не го показваме никъде като информация
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:43:10 PM
2:43
ние имаме някаква история на сделката, имаме стейдж по време на импорта на активитито
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:43:22 PM
2:43
но тоя стейдж го ползваме само за това търсене в on demand
(edited)
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:43:52 PM
2:43
преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insights
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:03 PM
2:44
имаме дата на отваряне и затваряне на сделка
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:21 PM
2:44
защо да не сложим и един маркер кога се е променил стейджа
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:31 PM
2:44
така в таймлайна ще се вижда “развитието” на тая сделка
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 2:45:09 PM
2:45 PM
хм ами то дали да го няма вече
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Vasil Vasilev
Today at 2:45:19 PM
2:45 PM
няма го
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 2:46:46 PM
2:46 PM
няма да
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:46:56 PM
2:46
но BE си го връща
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:47:01 PM
2:47
с тази цел точно
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Vasil Vasilev
Today at 2:47:07 PM
2:47 PM
знам, проверих, преди да предложа на Галя да ги покажем
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:47:09 PM
2:47
възможно
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:47:11 PM
2:47
но явно не е направено
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Vasil Vasilev
Today at 3:04:09 PM
3:04 PM
в крайна сметка тоя метод Activity::updateActivityCrmData се вика само при подмяна на opportunity, нали така?
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions...
|
[{"role":"AXPopUpButton","text [{"role":"AXPopUpButton","text":"Switch workspaces… (Jiminny Inc) Has new messages","depth":14,"bounds":{"left":0.011805556,"top":0.08111111,"width":0.025,"height":0.04},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Home","depth":14,"bounds":{"left":0.00625,"top":0.14,"width":0.036111113,"height":0.075555556},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Home","depth":16,"bounds":{"left":0.013888889,"top":0.19222222,"width":0.020833334,"height":0.015555556},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"DMs","depth":14,"bounds":{"left":0.00625,"top":0.21555555,"width":0.036111113,"height":0.075555556},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"DMs","depth":16,"bounds":{"left":0.015972223,"top":0.26777777,"width":0.016666668,"height":0.015555556},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Activity","depth":14,"bounds":{"left":0.00625,"top":0.2911111,"width":0.036111113,"height":0.075555556},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Activity","depth":16,"bounds":{"left":0.011111111,"top":0.34333333,"width":0.027083334,"height":0.015555556},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.011111111,"top":0.34333333,"width":0.0055555557,"height":0.015555556}},{"char_start":1,"char_count":7,"bounds":{"left":0.015972223,"top":0.34333333,"width":0.022222223,"height":0.015555556}}],"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":14,"bounds":{"left":0.00625,"top":0.36666667,"width":0.036111113,"height":0.075555556},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":16,"bounds":{"left":0.016666668,"top":0.4188889,"width":0.015972223,"height":0.015555556},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.016666668,"top":0.4188889,"width":0.004166667,"height":0.015555556}},{"char_start":1,"char_count":4,"bounds":{"left":0.020833334,"top":0.4188889,"width":0.011805556,"height":0.015555556}}],"role_description":"text"},{"role":"AXRadioButton","text":"Later","depth":14,"bounds":{"left":0.00625,"top":0.4422222,"width":0.036111113,"height":0.075555556},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Later","depth":16,"bounds":{"left":0.015277778,"top":0.49444443,"width":0.018055556,"height":0.015555556},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"More…","depth":14,"bounds":{"left":0.00625,"top":0.5177778,"width":0.036111113,"height":0.075555556},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More","depth":16,"bounds":{"left":0.015277778,"top":0.57,"width":0.01875,"height":0.015555556},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Unreads","depth":21,"bounds":{"left":0.077083334,"top":0.12777779,"width":0.039583333,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Threads","depth":21,"bounds":{"left":0.077083334,"top":0.12777779,"width":0.036805555,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Huddles","depth":21,"bounds":{"left":0.077083334,"top":0.12777779,"width":0.038194444,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drafts & sent","depth":21,"bounds":{"left":0.077083334,"top":0.12777779,"width":0.06111111,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":21,"bounds":{"left":0.18472221,"top":0.12777779,"width":0.0055555557,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Directories","depth":21,"bounds":{"left":0.077083334,"top":0.12777779,"width":0.050694443,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-x-integration-app","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.09166667,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"platform-inner-team","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.093055554,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ai-chapter","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.046527777,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"alerts","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.025694445,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"backend","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.038194444,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"bugs","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.022222223,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"confusion-clinic","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.072222225,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"curiosity_lab","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.057638887,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"engineering","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.054166667,"height":0.012222222},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"general","depth":23,"bounds":{"left":0.088194445,"top":0.15111111,"width":0.034027778,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-bg","depth":23,"bounds":{"left":0.088194445,"top":0.18222222,"width":0.048611112,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"platform-tickets","depth":23,"bounds":{"left":0.088194445,"top":0.21333334,"width":0.072916664,"height":0.02},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.088194445,"top":0.21333334,"width":0.00625,"height":0.02}},{"char_start":1,"char_count":15,"bounds":{"left":0.094444446,"top":0.21333334,"width":0.06666667,"height":0.02}}],"role_description":"text"},{"role":"AXStaticText","text":"product_launches","depth":23,"bounds":{"left":0.088194445,"top":0.24444444,"width":0.08055556,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"random","depth":23,"bounds":{"left":0.088194445,"top":0.27555555,"width":0.035416666,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"releases","depth":23,"bounds":{"left":0.088194445,"top":0.30666667,"width":0.038194444,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"sofia-office","depth":23,"bounds":{"left":0.088194445,"top":0.33777776,"width":0.05138889,"height":0.02},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.088194445,"top":0.33777776,"width":0.0048611113,"height":0.02}},{"char_start":1,"char_count":11,"bounds":{"left":0.093055554,"top":0.33777776,"width":0.045833334,"height":0.02}}],"role_description":"text"},{"role":"AXStaticText","text":"support","depth":23,"bounds":{"left":0.088194445,"top":0.36888888,"width":0.036111113,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"thank-yous","depth":23,"bounds":{"left":0.088194445,"top":0.4,"width":0.05138889,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"the_people_of_jiminny","depth":23,"bounds":{"left":0.088194445,"top":0.4311111,"width":0.094444446,"height":0.02},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.088194445,"top":0.4311111,"width":0.004166667,"height":0.02}},{"char_start":1,"char_count":20,"bounds":{"left":0.09236111,"top":0.4311111,"width":0.09861111,"height":0.02}}],"role_description":"text"},{"role":"AXStaticText","text":"Vasil Vasilev","depth":23,"bounds":{"left":0.088194445,"top":0.5044444,"width":0.055555556,"height":0.02},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.088194445,"top":0.5044444,"width":0.00625,"height":0.02}},{"char_start":1,"char_count":12,"bounds":{"left":0.094444446,"top":0.5044444,"width":0.048611112,"height":0.02}}],"role_description":"text"},{"role":"AXStaticText","text":"Galya Dimitrova","depth":23,"bounds":{"left":0.088194445,"top":0.53555554,"width":0.07361111,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"bounds":{"left":0.088194445,"top":0.56666666,"width":0.07847222,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Stefka Stoyanova","depth":23,"bounds":{"left":0.088194445,"top":0.5977778,"width":0.079166666,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Todor Stamatov","depth":23,"bounds":{"left":0.088194445,"top":0.6288889,"width":0.072222225,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Mario Georgiev","depth":23,"bounds":{"left":0.088194445,"top":0.66,"width":0.07152778,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":23,"bounds":{"left":0.088194445,"top":0.6911111,"width":0.06736111,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"James Graham","depth":23,"bounds":{"left":0.088194445,"top":0.7222222,"width":0.06666667,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tanev","depth":23,"bounds":{"left":0.088194445,"top":0.75333333,"width":0.060416665,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Steliyan Georgiev","depth":23,"bounds":{"left":0.088194445,"top":0.78444445,"width":0.07986111,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Petko Kashinski","depth":23,"bounds":{"left":0.088194445,"top":0.8155556,"width":0.072222225,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Lukas Kovalik","depth":23,"bounds":{"left":0.088194445,"top":0.8466667,"width":0.061805554,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"you","depth":23,"bounds":{"left":0.15555556,"top":0.8466667,"width":0.013194445,"height":0.02},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.15555556,"top":0.8466667,"width":0.0048611113,"height":0.02}},{"char_start":1,"char_count":2,"bounds":{"left":0.16041666,"top":0.8466667,"width":0.011805556,"height":0.02}}],"role_description":"text"},{"role":"AXStaticText","text":"Toast","depth":23,"bounds":{"left":0.088194445,"top":0.92,"width":0.025694445,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jira Cloud","depth":23,"bounds":{"left":0.088194445,"top":0.95111114,"width":0.045833334,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Google Calendar","depth":23,"bounds":{"left":0.088194445,"top":0.9822222,"width":0.06388889,"height":0.012222222},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Messages","depth":17,"bounds":{"left":0.21319444,"top":0.12777779,"width":0.06458333,"height":0.04222222},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Messages","depth":19,"bounds":{"left":0.2326389,"top":0.14,"width":0.039583333,"height":0.017777778},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Add canvas","depth":18,"bounds":{"left":0.27986112,"top":0.12777779,"width":0.07152778,"height":0.04222222},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Add canvas","depth":20,"bounds":{"left":0.29930556,"top":0.14,"width":0.046527777,"height":0.017777778},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":17,"bounds":{"left":0.35347223,"top":0.12777779,"width":0.04375,"height":0.04222222},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":19,"bounds":{"left":0.37291667,"top":0.14,"width":0.01875,"height":0.017777778},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.37291667,"top":0.14,"width":0.0055555557,"height":0.017777778}},{"char_start":1,"char_count":4,"bounds":{"left":0.3784722,"top":0.14,"width":0.013194445,"height":0.017777778}}],"role_description":"text"},{"role":"AXRadioButton","text":"Pins","depth":17,"bounds":{"left":0.4,"top":0.12777779,"width":0.04236111,"height":0.04222222},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Pins","depth":19,"bounds":{"left":0.41944444,"top":0.14,"width":0.017361112,"height":0.017777778},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Add and Edit Channel Tabs","depth":17,"bounds":{"left":0.44444445,"top":0.12777779,"width":0.022916667,"height":0.04222222},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Canvas","depth":17,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"List","depth":17,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Folder","depth":17,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.06458333,"height":0.0011111111},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.31111112,"top":0.16111112,"width":0.0055555557,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:33:54 PM","depth":24,"bounds":{"left":0.31666666,"top":0.16111112,"width":0.03125,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:33 PM","depth":25,"bounds":{"left":0.31666666,"top":0.16111112,"width":0.03125,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ами мисля че трием lead и негов стаге тогава","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.21736111,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:33:59 PM","depth":25,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:33","depth":26,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"трябва да го проверя","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.10208333,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Vasil Vasilev","depth":24,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.057638887,"height":0.0011111111},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.30416667,"top":0.16111112,"width":0.0055555557,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:35:14 PM","depth":24,"bounds":{"left":0.3090278,"top":0.16111112,"width":0.031944446,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:35 PM","depth":25,"bounds":{"left":0.3090278,"top":0.16111112,"width":0.031944446,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"наскоро рових по тоя код","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.12291667,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:35:33 PM","depth":25,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:35","depth":26,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"махаме lead id и подменяме stage id с това от сделката","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.26666668,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:35:48 PM","depth":25,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:35","depth":26,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"въпроса е дали е редно да го правим от историческа гледна точка","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.32083333,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.06458333,"height":0.0011111111},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.31111112,"top":0.16111112,"width":0.0055555557,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:40:15 PM","depth":24,"bounds":{"left":0.31666666,"top":0.16111112,"width":0.03125,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:40 PM","depth":25,"bounds":{"left":0.31666666,"top":0.16111112,"width":0.03125,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ами тука не знам, като цяло това му беше идея","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.22847222,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:40:29 PM","depth":25,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:40","depth":26,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"да знаеш по кое време е направено activity","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.20763889,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:40:47 PM","depth":25,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:40","depth":26,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"но щом вече го сменяваме при lead","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.1701389,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:41:51 PM","depth":25,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:41","depth":26,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"не е линеарно една сделка или lead която се развива във времето, сега си скаче напред назад както го мачнем","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.53402776,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:42:30 PM","depth":25,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:42","depth":26,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ще пиша на Галя да я питам какво да правим","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.21597221,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Vasil Vasilev","depth":24,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.057638887,"height":0.015555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.30416667,"top":0.16111112,"width":0.0055555557,"height":0.013333334},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:42:41 PM","depth":24,"bounds":{"left":0.3090278,"top":0.16111112,"width":0.031944446,"height":0.013333334},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:42 PM","depth":25,"bounds":{"left":0.3090278,"top":0.16111112,"width":0.031944446,"height":0.013333334},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"моето е по скоро идея","depth":25,"bounds":{"left":0.24652778,"top":0.17888889,"width":0.10902778,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:42:54 PM","depth":25,"bounds":{"left":0.22430556,"top":0.21555555,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:42","depth":26,"bounds":{"left":0.22430556,"top":0.21555555,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"в момента не го правим, но и почти не го показваме никъде като информация","depth":25,"bounds":{"left":0.24652778,"top":0.21222222,"width":0.37708333,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:43:10 PM","depth":25,"bounds":{"left":0.22430556,"top":0.2488889,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:43","depth":26,"bounds":{"left":0.22430556,"top":0.2488889,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ние имаме някаква история на сделката, имаме стейдж по време на импорта на активитито","depth":25,"bounds":{"left":0.24652778,"top":0.24555555,"width":0.44444445,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:43:22 PM","depth":25,"bounds":{"left":0.22430556,"top":0.2822222,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:43","depth":26,"bounds":{"left":0.22430556,"top":0.2822222,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"но тоя стейдж го ползваме само за това търсене в on demand","depth":25,"bounds":{"left":0.24652778,"top":0.27888888,"width":0.29652777,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"bounds":{"left":0.54236114,"top":0.28111112,"width":0.0027777778,"height":0.017777778},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(edited)","depth":25,"bounds":{"left":0.5451389,"top":0.28111112,"width":0.029861111,"height":0.017777778},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.5451389,"top":0.28111112,"width":0.0020833334,"height":0.017777778}},{"char_start":1,"char_count":7,"bounds":{"left":0.5472222,"top":0.28111112,"width":0.027777778,"height":0.017777778}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"bounds":{"left":0.57430553,"top":0.28111112,"width":0.0027777778,"height":0.017777778},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:43:52 PM","depth":25,"bounds":{"left":0.22430556,"top":0.31555554,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:43","depth":26,"bounds":{"left":0.22430556,"top":0.31555554,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insights","depth":25,"bounds":{"left":0.24652778,"top":0.3122222,"width":0.59166664,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:44:03 PM","depth":25,"bounds":{"left":0.22430556,"top":0.34888887,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:44","depth":26,"bounds":{"left":0.22430556,"top":0.34888887,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"имаме дата на отваряне и затваряне на сделка","depth":25,"bounds":{"left":0.24652778,"top":0.34555554,"width":0.225,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:44:21 PM","depth":25,"bounds":{"left":0.22430556,"top":0.38222224,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:44","depth":26,"bounds":{"left":0.22430556,"top":0.38222224,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"защо да не сложим и един маркер кога се е променил стейджа","depth":25,"bounds":{"left":0.24652778,"top":0.37888888,"width":0.30625,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:44:31 PM","depth":25,"bounds":{"left":0.22430556,"top":0.41555557,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:44","depth":26,"bounds":{"left":0.22430556,"top":0.41555557,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"така в таймлайна ще се вижда “развитието” на тая сделка","depth":25,"bounds":{"left":0.24652778,"top":0.41222224,"width":0.27986112,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"bounds":{"left":0.24652778,"top":0.44333333,"width":0.06458333,"height":0.024444444},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.31111112,"top":0.44555557,"width":0.0055555557,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:45:09 PM","depth":24,"bounds":{"left":0.31666666,"top":0.4488889,"width":0.03125,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:45 PM","depth":25,"bounds":{"left":0.31666666,"top":0.4488889,"width":0.03125,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"хм ами то дали да го няма вече","depth":25,"bounds":{"left":0.24652778,"top":0.47,"width":0.15138888,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Vasil Vasilev","depth":24,"bounds":{"left":0.24652778,"top":0.5011111,"width":0.057638887,"height":0.024444444},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.30416667,"top":0.50333333,"width":0.0055555557,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:45:19 PM","depth":24,"bounds":{"left":0.3090278,"top":0.50666666,"width":0.031944446,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:45 PM","depth":25,"bounds":{"left":0.3090278,"top":0.50666666,"width":0.031944446,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"няма го","depth":25,"bounds":{"left":0.24652778,"top":0.5277778,"width":0.036805555,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"bounds":{"left":0.24652778,"top":0.5588889,"width":0.06458333,"height":0.024444444},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.31111112,"top":0.5611111,"width":0.0055555557,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:46:46 PM","depth":24,"bounds":{"left":0.31666666,"top":0.5644444,"width":0.03125,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:46 PM","depth":25,"bounds":{"left":0.31666666,"top":0.5644444,"width":0.03125,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"няма да","depth":25,"bounds":{"left":0.24652778,"top":0.58555555,"width":0.038194444,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:46:56 PM","depth":25,"bounds":{"left":0.22430556,"top":0.62222224,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:46","depth":26,"bounds":{"left":0.22430556,"top":0.62222224,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"но BE си го връща","depth":25,"bounds":{"left":0.24652778,"top":0.6188889,"width":0.0875,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:47:01 PM","depth":25,"bounds":{"left":0.22430556,"top":0.65555555,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:47","depth":26,"bounds":{"left":0.22430556,"top":0.65555555,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"с тази цел точно","depth":25,"bounds":{"left":0.24652778,"top":0.6522222,"width":0.08055556,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Vasil Vasilev","depth":24,"bounds":{"left":0.24652778,"top":0.68333334,"width":0.057638887,"height":0.024444444},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.30416667,"top":0.6855556,"width":0.0055555557,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:47:07 PM","depth":24,"bounds":{"left":0.3090278,"top":0.6888889,"width":0.031944446,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:47 PM","depth":25,"bounds":{"left":0.3090278,"top":0.6888889,"width":0.031944446,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"знам, проверих, преди да предложа на Галя да ги покажем","depth":25,"bounds":{"left":0.24652778,"top":0.71,"width":0.28402779,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:47:09 PM","depth":25,"bounds":{"left":0.22430556,"top":0.74666667,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:47","depth":26,"bounds":{"left":0.22430556,"top":0.74666667,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"възможно","depth":25,"bounds":{"left":0.24652778,"top":0.74333334,"width":0.049305554,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:47:11 PM","depth":25,"bounds":{"left":0.22430556,"top":0.78,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:47","depth":26,"bounds":{"left":0.22430556,"top":0.78,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"но явно не е направено","depth":25,"bounds":{"left":0.24652778,"top":0.77666664,"width":0.11180556,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Vasil Vasilev","depth":24,"bounds":{"left":0.24652778,"top":0.80777776,"width":0.057638887,"height":0.024444444},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.30416667,"top":0.81,"width":0.0055555557,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 3:04:09 PM","depth":24,"bounds":{"left":0.3090278,"top":0.81333333,"width":0.031944446,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3:04 PM","depth":25,"bounds":{"left":0.3090278,"top":0.81333333,"width":0.031944446,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"в крайна сметка тоя метод Activity::updateActivityCrmData се вика само при подмяна на opportunity, нали така?","depth":25,"bounds":{"left":0.24652778,"top":0.83444446,"width":0.5347222,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1715786407851265108
|
-1568565684325660592
|
click
|
hybrid
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
1
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
bugs
confusion-clinic
curiosity_lab
engineering
general
jiminny-bg
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Vasil Vasilev
Galya Dimitrova
Aneliya Angelova
Stefka Stoyanova
Todor Stamatov
Mario Georgiev
Nikolay Ivanov
James Graham
Stoyan Tanev
Steliyan Georgiev
Petko Kashinski
Lukas Kovalik
you
Toast
Jira Cloud
Google Calendar
Messages
Messages
Add canvas
Add canvas
Files
Files
Pins
Pins
Add and Edit Channel Tabs
Canvas
List
Folder
Lukas Kovalik
Today at 2:33:54 PM
2:33 PM
ами мисля че трием lead и негов стаге тогава
Today at 2:33:59 PM
2:33
трябва да го проверя
Vasil Vasilev
Today at 2:35:14 PM
2:35 PM
наскоро рових по тоя код
Today at 2:35:33 PM
2:35
махаме lead id и подменяме stage id с това от сделката
Today at 2:35:48 PM
2:35
въпроса е дали е редно да го правим от историческа гледна точка
Lukas Kovalik
Today at 2:40:15 PM
2:40 PM
ами тука не знам, като цяло това му беше идея
Today at 2:40:29 PM
2:40
да знаеш по кое време е направено activity
Today at 2:40:47 PM
2:40
но щом вече го сменяваме при lead
Today at 2:41:51 PM
2:41
не е линеарно една сделка или lead която се развива във времето, сега си скаче напред назад както го мачнем
Today at 2:42:30 PM
2:42
ще пиша на Галя да я питам какво да правим
Vasil Vasilev
Today at 2:42:41 PM
2:42 PM
моето е по скоро идея
Today at 2:42:54 PM
2:42
в момента не го правим, но и почти не го показваме никъде като информация
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:43:10 PM
2:43
ние имаме някаква история на сделката, имаме стейдж по време на импорта на активитито
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:43:22 PM
2:43
но тоя стейдж го ползваме само за това търсене в on demand
(edited)
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:43:52 PM
2:43
преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insights
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:03 PM
2:44
имаме дата на отваряне и затваряне на сделка
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:21 PM
2:44
защо да не сложим и един маркер кога се е променил стейджа
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:31 PM
2:44
така в таймлайна ще се вижда “развитието” на тая сделка
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 2:45:09 PM
2:45 PM
хм ами то дали да го няма вече
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Vasil Vasilev
Today at 2:45:19 PM
2:45 PM
няма го
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 2:46:46 PM
2:46 PM
няма да
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:46:56 PM
2:46
но BE си го връща
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:47:01 PM
2:47
с тази цел точно
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Vasil Vasilev
Today at 2:47:07 PM
2:47 PM
знам, проверих, преди да предложа на Галя да ги покажем
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:47:09 PM
2:47
възможно
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:47:11 PM
2:47
но явно не е направено
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Vasil Vasilev
Today at 3:04:09 PM
3:04 PM
в крайна сметка тоя метод Activity::updateActivityCrmData се вика само при подмяна на opportunity, нали така?
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
HomeDMsActivityFilesLater..•MoreSlackFileEditViewEDJiminny ...# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Direct messages€. Vasil VasilevGalya DimitrovaP. Aneliya AngelovaStefka Stoyanova* Todor StamatovMario GeorgievNikolay Ivanovdo James Graham. Stoyan TanevRo Steliyan Georgiev. Petko KashinskiLukas Kovalik y...:: AppsToastJira CloudConala CalaGoHistoryWindowHelp‹ >0 llSupport Daily • nowQDescribe what you are looking forVasil VasilevSupport Dailynow - 15:00-15:15= Notes - Support Daily.MessagesAdd canvas@ Files& Pins +моето е по скоро идеяв момента не го правим, но и почти не го показваме никьде като информацияние имаме някаква история на сделката, имаме стейдж по време на импорта на активититоно тоя стейдж го ползваме само за това тьрсене в on demand (edited)преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insightsимаме дата на отваряне и затваряне на сделказащо да не сложим и един маркер кога се е променил стейджатака в таймлайна ще се вижда "развитието" на тая сделкаLukas Kovalik 2:45 PMхм ами то дали да го няма вечеVasil Vasilev 2:45 PMняма гоLukas Kovalik 2:46 PMняма дано ВЕ си го врьщас тази цел точноVasil Vasilev 2:47 PMзнам, проверих, преди да предложа на Галя да ги покажемВъзможноно явно не е направеноVasil Vasilev 3:04 PMв крайна сметка тоя метод Activity::updateActivityCrmData се вика само при подмяна на opportunity, нали така?Message Vasil Vasilev+Аal100% C 8• Thu 14 May 15:05:44C Join Google Meet...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
42825
|
1568
|
9
|
2026-05-14T12:05:45.296382+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760345296_m2.jpg...
|
Slack
|
Vasil Vasilev (DM) - Jiminny Inc - 3 new items - S Vasil Vasilev (DM) - Jiminny Inc - 3 new items - Slack...
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
1
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
bugs
confusion-clinic
curiosity_lab
engineering
general
jiminny-bg
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Vasil Vasilev
Galya Dimitrova
Aneliya Angelova
Stefka Stoyanova
Todor Stamatov
Mario Georgiev
Nikolay Ivanov
James Graham
Stoyan Tanev
Steliyan Georgiev
Petko Kashinski
Lukas Kovalik
you
Toast
Jira Cloud
Google Calendar
Messages
Messages
Add canvas
Add canvas
Files
Files
Pins
Pins
Add and Edit Channel Tabs
Canvas
List
Folder
Lukas Kovalik
Today at 2:33:54 PM
2:33 PM
ами мисля че трием lead и негов стаге тогава
Today at 2:33:59 PM
2:33
трябва да го проверя
Vasil Vasilev
Today at 2:35:14 PM
2:35 PM
наскоро рових по тоя код
Today at 2:35:33 PM
2:35
махаме lead id и подменяме stage id с това от сделката
Today at 2:35:48 PM
2:35
въпроса е дали е редно да го правим от историческа гледна точка
Lukas Kovalik
Today at 2:40:15 PM
2:40 PM
ами тука не знам, като цяло това му беше идея
Today at 2:40:29 PM
2:40
да знаеш по кое време е направено activity
Today at 2:40:47 PM
2:40
но щом вече го сменяваме при lead
Today at 2:41:51 PM
2:41
не е линеарно една сделка или lead която се развива във времето, сега си скаче напред назад както го мачнем
Today at 2:42:30 PM
2:42
ще пиша на Галя да я питам какво да правим
Vasil Vasilev
Today at 2:42:41 PM
2:42 PM
моето е по скоро идея
Today at 2:42:54 PM
2:42
в момента не го правим, но и почти не го показваме никъде като информация
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:43:10 PM
2:43
ние имаме някаква история на сделката, имаме стейдж по време на импорта на активитито
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:43:22 PM
2:43
но тоя стейдж го ползваме само за това търсене в on demand
(edited)
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:43:52 PM
2:43
преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insights
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:03 PM
2:44
имаме дата на отваряне и затваряне на сделка
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:21 PM
2:44
защо да не сложим и един маркер кога се е променил стейджа
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:31 PM
2:44
така в таймлайна ще се вижда “развитието” на тая сделка
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 2:45:09 PM
2:45 PM
хм ами то дали да го няма вече
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Vasil Vasilev
Today at 2:45:19 PM
2:45 PM
няма го
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 2:46:46 PM
2:46 PM
няма да
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:46:56 PM
2:46
но BE си го връща
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:47:01 PM
2:47
с тази цел точно
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Vasil Vasilev
Today at 2:47:07 PM
2:47 PM
знам, проверих, преди да предложа на Галя да ги покажем
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:47:09 PM
2:47
възможно
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:47:11 PM
2:47
но явно не е направено
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Vasil Vasilev
Today at 3:04:09 PM
3:04 PM
в крайна сметка тоя метод Activity::updateActivityCrmData се вика само при подмяна на opportunity, нали така?
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Conversation marked as unread from this message
Vasil Vasilev is typing...
|
[{"role":"AXPopUpButton","text [{"role":"AXPopUpButton","text":"Switch workspaces… (Jiminny Inc) Has new messages","depth":14,"bounds":{"left":0.27593085,"top":1.0,"width":0.011968086,"height":-0.058260202},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Home","depth":14,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Home","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"DMs","depth":14,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"DMs","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Activity","depth":14,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Activity","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":14,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Later","depth":14,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Later","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"More…","depth":14,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Unreads","depth":21,"bounds":{"left":0.30718085,"top":1.0,"width":0.018949468,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Threads","depth":21,"bounds":{"left":0.30718085,"top":1.0,"width":0.01761968,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Huddles","depth":21,"bounds":{"left":0.30718085,"top":1.0,"width":0.018284574,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drafts & sent","depth":21,"bounds":{"left":0.30718085,"top":1.0,"width":0.02925532,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":21,"bounds":{"left":0.3587101,"top":1.0,"width":0.0026595744,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Directories","depth":21,"bounds":{"left":0.30718085,"top":1.0,"width":0.024268618,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-x-integration-app","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.043882977,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"platform-inner-team","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.04454787,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ai-chapter","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.022273935,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"alerts","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.012300532,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"backend","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.018284574,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"bugs","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.010638298,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"confusion-clinic","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.034574468,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"curiosity_lab","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.027593086,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"engineering","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.025930852,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"general","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-bg","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"platform-tickets","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"product_launches","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"random","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"releases","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"sofia-office","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"support","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"thank-yous","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"the_people_of_jiminny","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Vasil Vasilev","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Galya Dimitrova","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Stefka Stoyanova","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Todor Stamatov","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Mario Georgiev","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"James Graham","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tanev","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Steliyan Georgiev","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Petko Kashinski","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Lukas Kovalik","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"you","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Toast","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jira Cloud","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Google Calendar","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Messages","depth":17,"bounds":{"left":0.3723404,"top":1.0,"width":0.030917553,"height":-0.09177971},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Messages","depth":19,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Add canvas","depth":18,"bounds":{"left":0.40425533,"top":1.0,"width":0.034242023,"height":-0.09177971},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Add canvas","depth":20,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":17,"bounds":{"left":0.43949467,"top":1.0,"width":0.020944148,"height":-0.09177971},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":19,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Pins","depth":17,"bounds":{"left":0.46176863,"top":1.0,"width":0.020279255,"height":-0.09177971},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Pins","depth":19,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Add and Edit Channel Tabs","depth":17,"bounds":{"left":0.48304522,"top":1.0,"width":0.010970744,"height":-0.09177971},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Canvas","depth":17,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"List","depth":17,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Folder","depth":17,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:33:54 PM","depth":24,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:33 PM","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ами мисля че трием lead и негов стаге тогава","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:33:59 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:33","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"трябва да го проверя","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Vasil Vasilev","depth":24,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:35:14 PM","depth":24,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:35 PM","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"наскоро рових по тоя код","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:35:33 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:35","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"махаме lead id и подменяме stage id с това от сделката","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:35:48 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:35","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"въпроса е дали е редно да го правим от историческа гледна точка","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:40:15 PM","depth":24,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:40 PM","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ами тука не знам, като цяло това му беше идея","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:40:29 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:40","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"да знаеш по кое време е направено activity","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:40:47 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:40","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"но щом вече го сменяваме при lead","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:41:51 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:41","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"не е линеарно една сделка или lead която се развива във времето, сега си скаче напред назад както го мачнем","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:42:30 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:42","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ще пиша на Галя да я питам какво да правим","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Vasil Vasilev","depth":24,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:42:41 PM","depth":24,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:42 PM","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"моето е по скоро идея","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:42:54 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:42","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"в момента не го правим, но и почти не го показваме никъде като информация","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:43:10 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:43","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ние имаме някаква история на сделката, имаме стейдж по време на импорта на активитито","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:43:22 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:43","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"но тоя стейдж го ползваме само за това търсене в on demand","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(edited)","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:43:52 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:43","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insights","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:44:03 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:44","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"имаме дата на отваряне и затваряне на сделка","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:44:21 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:44","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"защо да не сложим и един маркер кога се е променил стейджа","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:44:31 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:44","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"така в таймлайна ще се вижда “развитието” на тая сделка","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:45:09 PM","depth":24,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:45 PM","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"хм ами то дали да го няма вече","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Vasil Vasilev","depth":24,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:45:19 PM","depth":24,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:45 PM","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"няма го","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:46:46 PM","depth":24,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:46 PM","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"няма да","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:46:56 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:46","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"но BE си го връща","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:47:01 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:47","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"с тази цел точно","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Vasil Vasilev","depth":24,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:47:07 PM","depth":24,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:47 PM","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"знам, проверих, преди да предложа на Галя да ги покажем","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:47:09 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:47","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"възможно","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:47:11 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:47","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"но явно не е направено","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Vasil Vasilev","depth":24,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 3:04:09 PM","depth":24,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3:04 PM","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"в крайна сметка тоя метод Activity::updateActivityCrmData се вика само при подмяна на opportunity, нали така?","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"","depth":23,"on_screen":true,"value":"","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Conversation marked as unread from this message","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Vasil Vasilev is typing","depth":11,"on_screen":false,"role_description":"text"}]...
|
679030966992070548
|
-1568565684325660336
|
click
|
hybrid
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
1
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
bugs
confusion-clinic
curiosity_lab
engineering
general
jiminny-bg
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Vasil Vasilev
Galya Dimitrova
Aneliya Angelova
Stefka Stoyanova
Todor Stamatov
Mario Georgiev
Nikolay Ivanov
James Graham
Stoyan Tanev
Steliyan Georgiev
Petko Kashinski
Lukas Kovalik
you
Toast
Jira Cloud
Google Calendar
Messages
Messages
Add canvas
Add canvas
Files
Files
Pins
Pins
Add and Edit Channel Tabs
Canvas
List
Folder
Lukas Kovalik
Today at 2:33:54 PM
2:33 PM
ами мисля че трием lead и негов стаге тогава
Today at 2:33:59 PM
2:33
трябва да го проверя
Vasil Vasilev
Today at 2:35:14 PM
2:35 PM
наскоро рових по тоя код
Today at 2:35:33 PM
2:35
махаме lead id и подменяме stage id с това от сделката
Today at 2:35:48 PM
2:35
въпроса е дали е редно да го правим от историческа гледна точка
Lukas Kovalik
Today at 2:40:15 PM
2:40 PM
ами тука не знам, като цяло това му беше идея
Today at 2:40:29 PM
2:40
да знаеш по кое време е направено activity
Today at 2:40:47 PM
2:40
но щом вече го сменяваме при lead
Today at 2:41:51 PM
2:41
не е линеарно една сделка или lead която се развива във времето, сега си скаче напред назад както го мачнем
Today at 2:42:30 PM
2:42
ще пиша на Галя да я питам какво да правим
Vasil Vasilev
Today at 2:42:41 PM
2:42 PM
моето е по скоро идея
Today at 2:42:54 PM
2:42
в момента не го правим, но и почти не го показваме никъде като информация
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:43:10 PM
2:43
ние имаме някаква история на сделката, имаме стейдж по време на импорта на активитито
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:43:22 PM
2:43
но тоя стейдж го ползваме само за това търсене в on demand
(edited)
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:43:52 PM
2:43
преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insights
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:03 PM
2:44
имаме дата на отваряне и затваряне на сделка
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:21 PM
2:44
защо да не сложим и един маркер кога се е променил стейджа
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:31 PM
2:44
така в таймлайна ще се вижда “развитието” на тая сделка
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 2:45:09 PM
2:45 PM
хм ами то дали да го няма вече
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Vasil Vasilev
Today at 2:45:19 PM
2:45 PM
няма го
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 2:46:46 PM
2:46 PM
няма да
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:46:56 PM
2:46
но BE си го връща
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:47:01 PM
2:47
с тази цел точно
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Vasil Vasilev
Today at 2:47:07 PM
2:47 PM
знам, проверих, преди да предложа на Галя да ги покажем
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:47:09 PM
2:47
възможно
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:47:11 PM
2:47
но явно не е направено
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Vasil Vasilev
Today at 3:04:09 PM
3:04 PM
в крайна сметка тоя метод Activity::updateActivityCrmData се вика само при подмяна на opportunity, нали така?
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Conversation marked as unread from this message
Vasil Vasilev is typing
PnostormFV faVsco.js#12077 on JY-20903-update_activity-stProletey© TextRelayService.phpC) DeleteForCoachecCom© Sso.php© ValidateSendingMessage.phpDownloaamissing lrack© ActivityRepository.phpc rixAcuivitiesopportunitC) HaraDeleteActivitiestol© HardDeleteActivitiesTe© EventserviceProvider.pnp© ProspectCache.php© StaleRecordValidator.phpc) hyoratecalwincrmba© HydrateDefaultActivitv1C) OpportunityRepository.php© UpdateSingleEntity.phpC) MatchActivityCrmData.php(c) Service.php© Client.pheC) InviteMeetinaBot.php© IterateActivities.phd© MiarateLocationFromC:© MonitorDialerActivities(OpportunitySynctrait.pho(C) PayloadBuilder.php(c) Confiquration.phpmA1л vC) MonitorMeetingcounte© MonitorMeetingEndCor19 04class ImportActzvityuypes implements shouldQueueC) MonitorMeetinostartco(t) MonitorMeetinaTrait.ohC) NotifvNotLodded.oho© NotPlayableEmail.phpC) PreMeetinaNotification© PreMeetingReminder.plc) ProheMediaSeamentselC) RoaccianTranscrintComC) ReindeyRecentActivitie(c) RetrvProspectsummary(C) SetProviderCanabilitiec(e) StatusCount.php© SyncMissingCallDisposi© UpdateActivityElasticS€© UpdateElasticSearch.pl•_ Analvucsw Calendarsyalerml> C Hubspot› IntearationApp> C Traits© AddLavoutEntities.php© AutoloaDelavedCommaC) Backfilll@pportunitvUseiC BullhornCommandAbstC) BullhornPinaCommand.C) BullhornSearchCommarC) BullhornSessioncomma@ CheckActivitvl.cagableC) CleanDunlicateSieldDat@ FullSvncOpportunityColC) LoaActivitiesCommandl@ ManageSvncStrategyC@ MatchOnnortunituAativ(C MiaratoDrovidor nhn© ProcessHubspotObject(e) DuracDelotodOnnortun* Create the event Listener..public function __construct(private readonly ResolveTeamCrmConnection ScrmResolver,private readonly FieldRepository $fieldRepository.private readonly PlaybookCategoryRepository Srepository,1...}* Import the standard Event/Task Type picklist options from the CRMpublic function handle(PlaybookCreated $event): voidSplaybook = Sevent->[URL_WITH_CREDENTIALS] HS_local [jiminny@localhost]& console [PROD]A console (EU]tiò accounts [EU]fii stages (EU]tid teams (EU]ImporbotkecoraingJoo.pnp© Activity.php(C) SixActivitiesOnnortunitv.nhn Xi& console SlAGiNGnamespace Jiminny console commanas Accivicles•use ...181class FixActivitiesOpportunity extends Command20 0protected ssignature = 'activity:f1x-opportunity {--from=} 1--to=}';public function handle(EmailHelper SemailHelper): int{...}1usadeorivate function detcrmservicecfeam steam: ?Serviceinterface.....1 usageorivate function processParticinantsdServicelnterface ScrmService,Emal Helner SemailHelner.Activity Cactivitv): void iSteam = Sactivity->getTeamSuser = Sactivity->getUserO›Sparticipants = Sactivity->getParticipantsO:foreach (Sparticipants as $participant)if (Sparticipant->getUserId !== null || $participant->getEmailAddress === null) {continue.Sema1LAddress = Sparticipant->qetzma1lAddressO*if (SemailHelper->isCompanyEmail(Steam, SemailAddress)) {continue:trysSopportunitv = $this->find0pportunitvInCrm($crmService. SemailAddress. Suser->qetId@):catch (Throwable Se) 4$this->error( string: 'Could not find opportunity:' . $e->getMessageO):Sopportunity = null.i* (Sonnontunitv === null){iSthis->npsetActivitvinnontunitv/Sactivitv)•continue:• Inu 14 May 10:00.42+0 ..B2^cascadePipedrive SDK EvaluaFixing Activity Stage DImport CRM Activity TI've implemented Fix 4 in two locations:1. Activitv::updateActivitvCrmdata (lines 2122-2126).hohoif (Sopportunity d& $this->opportunity_id !== Sopportunity→>id) ‹sches→opporcuniy1o = sopporcunetyo,Reset stage id when opportunity changes to avoid stale stage from previous opportunity matchSthis->stage_id = $stage?->id;Potential Issue: If Sstage is null, this will set stage id to null even it the new opportunity has a stage in the databaseThis could happen if the CRM lookup doesn't return stage informationRecommendation: Consider checkina if the opportunity has a stage id and usina that as a fallbackO phpif Sonnortunitv &s Sthis-sonnortunitv id l== Sonnortunitv-sidh «Sthis->opportunity id = sopportunity->id:to avoid stale stage from previous opportunity matchsthis->stage_id = $stage?->id ?? Sopportunity->stage_id;However, looking at the context, $stage is derived from the CRM lookup results, so if it's null, the opportunity might nothave stage intormation avallable. The current behavior is acceptable.2. FixActivities0pportunitv command (lines 123-128)oho'opportunity_id' => $opportunity->getId(),'stage 1d = sopportunitv->getstage.donNo iccues found - This correctly undates hoth fielde toaetherOther locations reviewed:•IntearationApp/Service.php (line 560: Sets stage id directly from opportunity→stage id when creatina/uodatinafrom lead data - this is correct.• Copy command: Copies both opportunity id and stage id together - this is correct.• ActivityController: Handles stage id from user requests - not related to the bugTest Results• UodateCrmbataTect : 2 tests passed (6 assertions)• MatchCrm0bjectTest : 15 tests passed (75 assertions)The imolementation is sound. The only minor concern is the null stage case in updateActivitvCrmDatao, but this isaccontablo aivon tha contovtfnl .• Your included weekly usage quota is exhausted. Purchase extra usage to continue using premium models. Quota resets May 17, 11:00Lets have÷ @Code SWF-1.6W Windsurf TeamoPo 4 spac...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
42826
|
NULL
|
0
|
2026-05-14T12:06:15.363211+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760375363_m1.jpg...
|
Slack
|
Vasil Vasilev (DM) - Jiminny Inc - 4 new items - S Vasil Vasilev (DM) - Jiminny Inc - 4 new items - Slack...
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
1
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
bugs
confusion-clinic
curiosity_lab
engineering
general
jiminny-bg
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Galya Dimitrova
Vasil Vasilev
Aneliya Angelova
Stefka Stoyanova
Todor Stamatov
Mario Georgiev
Nikolay Ivanov
James Graham
Stoyan Tanev
Steliyan Georgiev
Petko Kashinski
Lukas Kovalik
you
Toast
Jira Cloud
Google Calendar
Messages
Messages
Add canvas
Add canvas
Files
Files
Pins
Pins
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Vasil Vasilev
Today at 2:35:14 PM
2:35 PM
наскоро рових по тоя код
Today at 2:35:33 PM
2:35
махаме lead id и подменяме stage id с това от сделката
Today at 2:35:48 PM
2:35
въпроса е дали е редно да го правим от историческа гледна точка
Lukas Kovalik
Today at 2:40:15 PM
2:40 PM
ами тука не знам, като цяло това му беше идея
Today at 2:40:29 PM
2:40
да знаеш по кое време е направено activity
Today at 2:40:47 PM
2:40
но щом вече го сменяваме при lead
Today at 2:41:51 PM
2:41
не е линеарно една сделка или lead която се развива във времето, сега си скаче напред назад както го мачнем
Today at 2:42:30 PM
2:42
ще пиша на Галя да я питам какво да правим
Vasil Vasilev
Today at 2:42:41 PM
2:42 PM
моето е по скоро идея
Today at 2:42:54 PM
2:42
в момента не го правим, но и почти не го показваме никъде като информация
Today at 2:43:10 PM
2:43
ние имаме някаква история на сделката, имаме стейдж по време на импорта на активитито
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:43:22 PM
2:43
но тоя стейдж го ползваме само за това търсене в on demand
(edited)
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:43:52 PM
2:43
преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insights
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:03 PM
2:44
имаме дата на отваряне и затваряне на сделка
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:21 PM
2:44
защо да не сложим и един маркер кога се е променил стейджа
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:31 PM
2:44
така в таймлайна ще се вижда “развитието” на тая сделка
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 2:45:09 PM
2:45 PM
хм ами то дали да го няма вече
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Vasil Vasilev
Today at 2:45:19 PM
2:45 PM
няма го
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 2:46:46 PM
2:46 PM
няма да
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:46:56 PM
2:46
но BE си го връща
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:47:01 PM
2:47
с тази цел точно
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Vasil Vasilev
Today at 2:47:07 PM
2:47 PM
знам, проверих, преди да предложа на Галя да ги покажем
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:47:09 PM
2:47
възможно
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:47:11 PM
2:47
но явно не е направено
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Vasil Vasilev
Today at 3:04:09 PM
3:04 PM
в крайна сметка тоя метод Activity::updateActivityCrmData се вика само при подмяна на opportunity, нали така?
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 3:05:52 PM
3:05 PM
гледам го сега
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Conversation marked as unread from this message
Vasil Vasilev is typing...
|
[{"role":"AXPopUpButton","text [{"role":"AXPopUpButton","text":"Switch workspaces… (Jiminny Inc) Has new messages","depth":14,"bounds":{"left":0.011805556,"top":0.08111111,"width":0.025,"height":0.04},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Home","depth":14,"bounds":{"left":0.00625,"top":0.14,"width":0.036111113,"height":0.075555556},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Home","depth":16,"bounds":{"left":0.013888889,"top":0.19222222,"width":0.020833334,"height":0.015555556},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"DMs","depth":14,"bounds":{"left":0.00625,"top":0.21555555,"width":0.036111113,"height":0.075555556},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"DMs","depth":16,"bounds":{"left":0.015972223,"top":0.26777777,"width":0.016666668,"height":0.015555556},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Activity","depth":14,"bounds":{"left":0.00625,"top":0.2911111,"width":0.036111113,"height":0.075555556},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Activity","depth":16,"bounds":{"left":0.011111111,"top":0.34333333,"width":0.027083334,"height":0.015555556},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.011111111,"top":0.34333333,"width":0.0055555557,"height":0.015555556}},{"char_start":1,"char_count":7,"bounds":{"left":0.015972223,"top":0.34333333,"width":0.022222223,"height":0.015555556}}],"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":14,"bounds":{"left":0.00625,"top":0.36666667,"width":0.036111113,"height":0.075555556},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":16,"bounds":{"left":0.016666668,"top":0.4188889,"width":0.015972223,"height":0.015555556},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.016666668,"top":0.4188889,"width":0.004166667,"height":0.015555556}},{"char_start":1,"char_count":4,"bounds":{"left":0.020833334,"top":0.4188889,"width":0.011805556,"height":0.015555556}}],"role_description":"text"},{"role":"AXRadioButton","text":"Later","depth":14,"bounds":{"left":0.00625,"top":0.4422222,"width":0.036111113,"height":0.075555556},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Later","depth":16,"bounds":{"left":0.015277778,"top":0.49444443,"width":0.018055556,"height":0.015555556},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"More…","depth":14,"bounds":{"left":0.00625,"top":0.5177778,"width":0.036111113,"height":0.075555556},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More","depth":16,"bounds":{"left":0.015277778,"top":0.57,"width":0.01875,"height":0.015555556},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Unreads","depth":21,"bounds":{"left":0.077083334,"top":0.12777779,"width":0.039583333,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Threads","depth":21,"bounds":{"left":0.077083334,"top":0.12777779,"width":0.036805555,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Huddles","depth":21,"bounds":{"left":0.077083334,"top":0.12777779,"width":0.038194444,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drafts & sent","depth":21,"bounds":{"left":0.077083334,"top":0.12777779,"width":0.06111111,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":21,"bounds":{"left":0.18472221,"top":0.12777779,"width":0.0055555557,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Directories","depth":21,"bounds":{"left":0.077083334,"top":0.12777779,"width":0.050694443,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-x-integration-app","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.09166667,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"platform-inner-team","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.093055554,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ai-chapter","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.046527777,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"alerts","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.025694445,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"backend","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.038194444,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"bugs","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.022222223,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"confusion-clinic","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.072222225,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"curiosity_lab","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.057638887,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"engineering","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.054166667,"height":0.012222222},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"general","depth":23,"bounds":{"left":0.088194445,"top":0.15111111,"width":0.034027778,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-bg","depth":23,"bounds":{"left":0.088194445,"top":0.18222222,"width":0.048611112,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"platform-tickets","depth":23,"bounds":{"left":0.088194445,"top":0.21333334,"width":0.072916664,"height":0.02},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.088194445,"top":0.21333334,"width":0.00625,"height":0.02}},{"char_start":1,"char_count":15,"bounds":{"left":0.094444446,"top":0.21333334,"width":0.06666667,"height":0.02}}],"role_description":"text"},{"role":"AXStaticText","text":"product_launches","depth":23,"bounds":{"left":0.088194445,"top":0.24444444,"width":0.08055556,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"random","depth":23,"bounds":{"left":0.088194445,"top":0.27555555,"width":0.035416666,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"releases","depth":23,"bounds":{"left":0.088194445,"top":0.30666667,"width":0.038194444,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"sofia-office","depth":23,"bounds":{"left":0.088194445,"top":0.33777776,"width":0.05138889,"height":0.02},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.088194445,"top":0.33777776,"width":0.0048611113,"height":0.02}},{"char_start":1,"char_count":11,"bounds":{"left":0.093055554,"top":0.33777776,"width":0.045833334,"height":0.02}}],"role_description":"text"},{"role":"AXStaticText","text":"support","depth":23,"bounds":{"left":0.088194445,"top":0.36888888,"width":0.036111113,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"thank-yous","depth":23,"bounds":{"left":0.088194445,"top":0.4,"width":0.05138889,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"the_people_of_jiminny","depth":23,"bounds":{"left":0.088194445,"top":0.4311111,"width":0.094444446,"height":0.02},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.088194445,"top":0.4311111,"width":0.004166667,"height":0.02}},{"char_start":1,"char_count":20,"bounds":{"left":0.09236111,"top":0.4311111,"width":0.09861111,"height":0.02}}],"role_description":"text"},{"role":"AXStaticText","text":"Galya Dimitrova","depth":23,"bounds":{"left":0.088194445,"top":0.5044444,"width":0.06388889,"height":0.02},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.088194445,"top":0.5044444,"width":0.007638889,"height":0.02}},{"char_start":1,"char_count":14,"bounds":{"left":0.09583333,"top":0.5044444,"width":0.068055555,"height":0.02}}],"role_description":"text"},{"role":"AXStaticText","text":"Vasil Vasilev","depth":23,"bounds":{"left":0.088194445,"top":0.53555554,"width":0.055555556,"height":0.02},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.088194445,"top":0.53555554,"width":0.00625,"height":0.02}},{"char_start":1,"char_count":12,"bounds":{"left":0.094444446,"top":0.53555554,"width":0.048611112,"height":0.02}}],"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"bounds":{"left":0.088194445,"top":0.56666666,"width":0.07847222,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Stefka Stoyanova","depth":23,"bounds":{"left":0.088194445,"top":0.5977778,"width":0.079166666,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Todor Stamatov","depth":23,"bounds":{"left":0.088194445,"top":0.6288889,"width":0.072222225,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Mario Georgiev","depth":23,"bounds":{"left":0.088194445,"top":0.66,"width":0.07152778,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":23,"bounds":{"left":0.088194445,"top":0.6911111,"width":0.06736111,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"James Graham","depth":23,"bounds":{"left":0.088194445,"top":0.7222222,"width":0.06666667,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tanev","depth":23,"bounds":{"left":0.088194445,"top":0.75333333,"width":0.060416665,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Steliyan Georgiev","depth":23,"bounds":{"left":0.088194445,"top":0.78444445,"width":0.07986111,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Petko Kashinski","depth":23,"bounds":{"left":0.088194445,"top":0.8155556,"width":0.072222225,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Lukas Kovalik","depth":23,"bounds":{"left":0.088194445,"top":0.8466667,"width":0.061805554,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"you","depth":23,"bounds":{"left":0.15555556,"top":0.8466667,"width":0.013194445,"height":0.02},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.15555556,"top":0.8466667,"width":0.0048611113,"height":0.02}},{"char_start":1,"char_count":2,"bounds":{"left":0.16041666,"top":0.8466667,"width":0.011805556,"height":0.02}}],"role_description":"text"},{"role":"AXStaticText","text":"Toast","depth":23,"bounds":{"left":0.088194445,"top":0.92,"width":0.025694445,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jira Cloud","depth":23,"bounds":{"left":0.088194445,"top":0.95111114,"width":0.045833334,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Google Calendar","depth":23,"bounds":{"left":0.088194445,"top":0.9822222,"width":0.06388889,"height":0.012222222},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Messages","depth":17,"bounds":{"left":0.21319444,"top":0.12777779,"width":0.06458333,"height":0.04222222},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Messages","depth":19,"bounds":{"left":0.2326389,"top":0.14,"width":0.039583333,"height":0.017777778},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Add canvas","depth":18,"bounds":{"left":0.27986112,"top":0.12777779,"width":0.07152778,"height":0.04222222},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Add canvas","depth":20,"bounds":{"left":0.29930556,"top":0.14,"width":0.046527777,"height":0.017777778},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":17,"bounds":{"left":0.35347223,"top":0.12777779,"width":0.04375,"height":0.04222222},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":19,"bounds":{"left":0.37291667,"top":0.14,"width":0.01875,"height":0.017777778},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.37291667,"top":0.14,"width":0.0055555557,"height":0.017777778}},{"char_start":1,"char_count":4,"bounds":{"left":0.3784722,"top":0.14,"width":0.013194445,"height":0.017777778}}],"role_description":"text"},{"role":"AXRadioButton","text":"Pins","depth":17,"bounds":{"left":0.4,"top":0.12777779,"width":0.04236111,"height":0.04222222},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Pins","depth":19,"bounds":{"left":0.41944444,"top":0.14,"width":0.017361112,"height":0.017777778},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Add and Edit Channel Tabs","depth":17,"bounds":{"left":0.44444445,"top":0.12777779,"width":0.022916667,"height":0.04222222},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Canvas","depth":17,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"List","depth":17,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Folder","depth":17,"on_screen":false,"role_description":"text"},{"role":"AXPopUpButton","text":"Jump to date","depth":23,"bounds":{"left":0.5729167,"top":0.17666666,"width":0.05277778,"height":0.031111112},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Vasil Vasilev","depth":24,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.057638887,"height":0.0011111111},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.30416667,"top":0.16111112,"width":0.0055555557,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:35:14 PM","depth":24,"bounds":{"left":0.3090278,"top":0.16111112,"width":0.031944446,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:35 PM","depth":25,"bounds":{"left":0.3090278,"top":0.16111112,"width":0.031944446,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"наскоро рових по тоя код","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.12291667,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:35:33 PM","depth":25,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:35","depth":26,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"махаме lead id и подменяме stage id с това от сделката","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.26666668,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:35:48 PM","depth":25,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:35","depth":26,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"въпроса е дали е редно да го правим от историческа гледна точка","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.32083333,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.06458333,"height":0.0011111111},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.31111112,"top":0.16111112,"width":0.0055555557,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:40:15 PM","depth":24,"bounds":{"left":0.31666666,"top":0.16111112,"width":0.03125,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:40 PM","depth":25,"bounds":{"left":0.31666666,"top":0.16111112,"width":0.03125,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ами тука не знам, като цяло това му беше идея","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.22847222,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:40:29 PM","depth":25,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:40","depth":26,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"да знаеш по кое време е направено activity","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.20763889,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:40:47 PM","depth":25,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:40","depth":26,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"но щом вече го сменяваме при lead","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.1701389,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:41:51 PM","depth":25,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:41","depth":26,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"не е линеарно една сделка или lead която се развива във времето, сега си скаче напред назад както го мачнем","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.53402776,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:42:30 PM","depth":25,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:42","depth":26,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ще пиша на Галя да я питам какво да правим","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.21597221,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Vasil Vasilev","depth":24,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.057638887,"height":0.0011111111},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.30416667,"top":0.16111112,"width":0.0055555557,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:42:41 PM","depth":24,"bounds":{"left":0.3090278,"top":0.16111112,"width":0.031944446,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:42 PM","depth":25,"bounds":{"left":0.3090278,"top":0.16111112,"width":0.031944446,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"моето е по скоро идея","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.10902778,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:42:54 PM","depth":25,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.013333334},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:42","depth":26,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.013333334},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"в момента не го правим, но и почти не го показваме никъде като информация","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.37708333,"height":0.013333334},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:43:10 PM","depth":25,"bounds":{"left":0.22430556,"top":0.19111112,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:43","depth":26,"bounds":{"left":0.22430556,"top":0.19111112,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ние имаме някаква история на сделката, имаме стейдж по време на импорта на активитито","depth":25,"bounds":{"left":0.24652778,"top":0.18777777,"width":0.44444445,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:43:22 PM","depth":25,"bounds":{"left":0.22430556,"top":0.22444445,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:43","depth":26,"bounds":{"left":0.22430556,"top":0.22444445,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"но тоя стейдж го ползваме само за това търсене в on demand","depth":25,"bounds":{"left":0.24652778,"top":0.2211111,"width":0.29652777,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"bounds":{"left":0.54236114,"top":0.22333333,"width":0.0027777778,"height":0.017777778},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(edited)","depth":25,"bounds":{"left":0.5451389,"top":0.22333333,"width":0.029861111,"height":0.017777778},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.5451389,"top":0.22333333,"width":0.0020833334,"height":0.017777778}},{"char_start":1,"char_count":7,"bounds":{"left":0.5472222,"top":0.22333333,"width":0.027777778,"height":0.017777778}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"bounds":{"left":0.57430553,"top":0.22333333,"width":0.0027777778,"height":0.017777778},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:43:52 PM","depth":25,"bounds":{"left":0.22430556,"top":0.25777778,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:43","depth":26,"bounds":{"left":0.22430556,"top":0.25777778,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insights","depth":25,"bounds":{"left":0.24652778,"top":0.25444445,"width":0.59166664,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:44:03 PM","depth":25,"bounds":{"left":0.22430556,"top":0.2911111,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:44","depth":26,"bounds":{"left":0.22430556,"top":0.2911111,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"имаме дата на отваряне и затваряне на сделка","depth":25,"bounds":{"left":0.24652778,"top":0.28777778,"width":0.225,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:44:21 PM","depth":25,"bounds":{"left":0.22430556,"top":0.32444444,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:44","depth":26,"bounds":{"left":0.22430556,"top":0.32444444,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"защо да не сложим и един маркер кога се е променил стейджа","depth":25,"bounds":{"left":0.24652778,"top":0.3211111,"width":0.30625,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:44:31 PM","depth":25,"bounds":{"left":0.22430556,"top":0.35777777,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:44","depth":26,"bounds":{"left":0.22430556,"top":0.35777777,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"така в таймлайна ще се вижда “развитието” на тая сделка","depth":25,"bounds":{"left":0.24652778,"top":0.35444444,"width":0.27986112,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"bounds":{"left":0.24652778,"top":0.38555557,"width":0.06458333,"height":0.024444444},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.31111112,"top":0.38777778,"width":0.0055555557,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:45:09 PM","depth":24,"bounds":{"left":0.31666666,"top":0.3911111,"width":0.03125,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:45 PM","depth":25,"bounds":{"left":0.31666666,"top":0.3911111,"width":0.03125,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"хм ами то дали да го няма вече","depth":25,"bounds":{"left":0.24652778,"top":0.41222224,"width":0.15138888,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Vasil Vasilev","depth":24,"bounds":{"left":0.24652778,"top":0.44333333,"width":0.057638887,"height":0.024444444},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.30416667,"top":0.44555557,"width":0.0055555557,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:45:19 PM","depth":24,"bounds":{"left":0.3090278,"top":0.4488889,"width":0.031944446,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:45 PM","depth":25,"bounds":{"left":0.3090278,"top":0.4488889,"width":0.031944446,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"няма го","depth":25,"bounds":{"left":0.24652778,"top":0.47,"width":0.036805555,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"bounds":{"left":0.24652778,"top":0.5011111,"width":0.06458333,"height":0.024444444},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.31111112,"top":0.50333333,"width":0.0055555557,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:46:46 PM","depth":24,"bounds":{"left":0.31666666,"top":0.50666666,"width":0.03125,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:46 PM","depth":25,"bounds":{"left":0.31666666,"top":0.50666666,"width":0.03125,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"няма да","depth":25,"bounds":{"left":0.24652778,"top":0.5277778,"width":0.038194444,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:46:56 PM","depth":25,"bounds":{"left":0.22430556,"top":0.5644444,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:46","depth":26,"bounds":{"left":0.22430556,"top":0.5644444,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"но BE си го връща","depth":25,"bounds":{"left":0.24652778,"top":0.5611111,"width":0.0875,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:47:01 PM","depth":25,"bounds":{"left":0.22430556,"top":0.5977778,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:47","depth":26,"bounds":{"left":0.22430556,"top":0.5977778,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"с тази цел точно","depth":25,"bounds":{"left":0.24652778,"top":0.59444445,"width":0.08055556,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Vasil Vasilev","depth":24,"bounds":{"left":0.24652778,"top":0.6255556,"width":0.057638887,"height":0.024444444},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.30416667,"top":0.62777776,"width":0.0055555557,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:47:07 PM","depth":24,"bounds":{"left":0.3090278,"top":0.6311111,"width":0.031944446,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:47 PM","depth":25,"bounds":{"left":0.3090278,"top":0.6311111,"width":0.031944446,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"знам, проверих, преди да предложа на Галя да ги покажем","depth":25,"bounds":{"left":0.24652778,"top":0.6522222,"width":0.28402779,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:47:09 PM","depth":25,"bounds":{"left":0.22430556,"top":0.6888889,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:47","depth":26,"bounds":{"left":0.22430556,"top":0.6888889,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"възможно","depth":25,"bounds":{"left":0.24652778,"top":0.6855556,"width":0.049305554,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:47:11 PM","depth":25,"bounds":{"left":0.22430556,"top":0.7222222,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:47","depth":26,"bounds":{"left":0.22430556,"top":0.7222222,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"но явно не е направено","depth":25,"bounds":{"left":0.24652778,"top":0.7188889,"width":0.11180556,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Vasil Vasilev","depth":24,"bounds":{"left":0.24652778,"top":0.75,"width":0.057638887,"height":0.024444444},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.30416667,"top":0.75222224,"width":0.0055555557,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 3:04:09 PM","depth":24,"bounds":{"left":0.3090278,"top":0.75555557,"width":0.031944446,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3:04 PM","depth":25,"bounds":{"left":0.3090278,"top":0.75555557,"width":0.031944446,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"в крайна сметка тоя метод Activity::updateActivityCrmData се вика само при подмяна на opportunity, нали така?","depth":25,"bounds":{"left":0.24652778,"top":0.77666664,"width":0.5347222,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"bounds":{"left":0.24652778,"top":0.80777776,"width":0.06458333,"height":0.024444444},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.31111112,"top":0.81,"width":0.0055555557,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 3:05:52 PM","depth":24,"bounds":{"left":0.31666666,"top":0.81333333,"width":0.03125,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3:05 PM","depth":25,"bounds":{"left":0.31666666,"top":0.81333333,"width":0.03125,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"гледам го сега","depth":25,"bounds":{"left":0.24652778,"top":0.83444446,"width":0.06944445,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"","depth":23,"bounds":{"left":0.21666667,"top":0.88,"width":0.7652778,"height":0.04222222},"on_screen":true,"value":"","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Conversation marked as unread from this message","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Vasil Vasilev is typing","depth":11,"on_screen":false,"role_description":"text"}]...
|
-1177022757992950536
|
-1573069284087248560
|
idle
|
hybrid
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
1
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
bugs
confusion-clinic
curiosity_lab
engineering
general
jiminny-bg
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Galya Dimitrova
Vasil Vasilev
Aneliya Angelova
Stefka Stoyanova
Todor Stamatov
Mario Georgiev
Nikolay Ivanov
James Graham
Stoyan Tanev
Steliyan Georgiev
Petko Kashinski
Lukas Kovalik
you
Toast
Jira Cloud
Google Calendar
Messages
Messages
Add canvas
Add canvas
Files
Files
Pins
Pins
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Vasil Vasilev
Today at 2:35:14 PM
2:35 PM
наскоро рових по тоя код
Today at 2:35:33 PM
2:35
махаме lead id и подменяме stage id с това от сделката
Today at 2:35:48 PM
2:35
въпроса е дали е редно да го правим от историческа гледна точка
Lukas Kovalik
Today at 2:40:15 PM
2:40 PM
ами тука не знам, като цяло това му беше идея
Today at 2:40:29 PM
2:40
да знаеш по кое време е направено activity
Today at 2:40:47 PM
2:40
но щом вече го сменяваме при lead
Today at 2:41:51 PM
2:41
не е линеарно една сделка или lead която се развива във времето, сега си скаче напред назад както го мачнем
Today at 2:42:30 PM
2:42
ще пиша на Галя да я питам какво да правим
Vasil Vasilev
Today at 2:42:41 PM
2:42 PM
моето е по скоро идея
Today at 2:42:54 PM
2:42
в момента не го правим, но и почти не го показваме никъде като информация
Today at 2:43:10 PM
2:43
ние имаме някаква история на сделката, имаме стейдж по време на импорта на активитито
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:43:22 PM
2:43
но тоя стейдж го ползваме само за това търсене в on demand
(edited)
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:43:52 PM
2:43
преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insights
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:03 PM
2:44
имаме дата на отваряне и затваряне на сделка
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:21 PM
2:44
защо да не сложим и един маркер кога се е променил стейджа
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:31 PM
2:44
така в таймлайна ще се вижда “развитието” на тая сделка
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 2:45:09 PM
2:45 PM
хм ами то дали да го няма вече
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Vasil Vasilev
Today at 2:45:19 PM
2:45 PM
няма го
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 2:46:46 PM
2:46 PM
няма да
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:46:56 PM
2:46
но BE си го връща
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:47:01 PM
2:47
с тази цел точно
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Vasil Vasilev
Today at 2:47:07 PM
2:47 PM
знам, проверих, преди да предложа на Галя да ги покажем
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:47:09 PM
2:47
възможно
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:47:11 PM
2:47
но явно не е направено
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Vasil Vasilev
Today at 3:04:09 PM
3:04 PM
в крайна сметка тоя метод Activity::updateActivityCrmData се вика само при подмяна на opportunity, нали така?
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 3:05:52 PM
3:05 PM
гледам го сега
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Conversation marked as unread from this message
Vasil Vasilev is typing
HomeDMsActivityFilesLater..•MoreSlackFileEditViewEDJiminny ...# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...• Direct messages••• Galya Dimit...€. Vasil VasilevP. Aneliya AngelovaStefka Stoyanova: Todor StamatovMario GeorgievNikolay Ivanovdo James Graham. Stoyan TanevRo Steliyan GeorgievPetko KashinskiLukas Kovalik y...::: AppsToastJira CloudConala CalaGoHistoryWindowHelp< →0 ahlSupport Daily • nowQDescribe what you are looking forVasil VasilevSupport Dailynow - 15:00-15:15= Notes - Support Daily.MessagesAdd canvasO Files& Pins +ние имаме някаква история на сделката, имаме стейдж по време наTodayна активититоно тоя стейдж го ползваме само за това тьрсене в on demand (edited)преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insightsимаме дата на отваряне и затваряне на сделказащо да не сложим и един маркер кога се е променил стейджатака в таймлайна ще се вижда "развитието" на тая сделкаLukas Kovalik 2:45 PMхм ами то дали да го няма вечеVasil Vasilev 2:45 PMняма гоLukas Kovalik 2:46 PMняма дано ВЕ си го врьщас тази цел точноVasil Vasilev 2:47 PMзнам, проверих, преди да предложа на Галя да ги покажемвЪЗМоЖНоно явно не е направеноVasil Vasilev 3:04 PMв крайна сметка тоя метод Activity:updateActivityCrmData се вика само при подмяна на opportunity, нали така?Lukas Kovalik 3:05 PMгледам го сегаMessage Vasil Vasilev+Аal100% C 8• Thu 14 May 15:06:14C Join Google Meet...
|
42824
|
NULL
|
NULL
|
NULL
|
|
42827
|
NULL
|
0
|
2026-05-14T12:06:16.220537+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760376220_m2.jpg...
|
Slack
|
Vasil Vasilev (DM) - Jiminny Inc - 4 new items - S Vasil Vasilev (DM) - Jiminny Inc - 4 new items - Slack...
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
1
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
bugs
confusion-clinic
curiosity_lab
engineering
general
jiminny-bg
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Galya Dimitrova
Vasil Vasilev
Aneliya Angelova
Stefka Stoyanova
Todor Stamatov
Mario Georgiev
Nikolay Ivanov
James Graham
Stoyan Tanev
Steliyan Georgiev
Petko Kashinski
Lukas Kovalik
you
Toast
Jira Cloud
Google Calendar
Messages
Messages
Add canvas
Add canvas
Files
Files
Pins
Pins
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Vasil Vasilev
Today at 2:35:14 PM
2:35 PM
наскоро рових по тоя код
Today at 2:35:33 PM
2:35
махаме lead id и подменяме stage id с това от сделката
Today at 2:35:48 PM
2:35
въпроса е дали е редно да го правим от историческа гледна точка
Lukas Kovalik
Today at 2:40:15 PM
2:40 PM
ами тука не знам, като цяло това му беше идея
Today at 2:40:29 PM
2:40
да знаеш по кое време е направено activity
Today at 2:40:47 PM
2:40
но щом вече го сменяваме при lead
Today at 2:41:51 PM
2:41
не е линеарно една сделка или lead която се развива във времето, сега си скаче напред назад както го мачнем
Today at 2:42:30 PM
2:42
ще пиша на Галя да я питам какво да правим
Vasil Vasilev
Today at 2:42:41 PM
2:42 PM
моето е по скоро идея
Today at 2:42:54 PM
2:42
в момента не го правим, но и почти не го показваме никъде като информация
Today at 2:43:10 PM
2:43
ние имаме някаква история на сделката, имаме стейдж по време на импорта на активитито
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:43:22 PM
2:43
но тоя стейдж го ползваме само за това търсене в on demand
(edited)
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:43:52 PM
2:43
преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insights
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:03 PM
2:44
имаме дата на отваряне и затваряне на сделка
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:21 PM
2:44
защо да не сложим и един маркер кога се е променил стейджа
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:31 PM...
|
[{"role":"AXPopUpButton","text [{"role":"AXPopUpButton","text":"Switch workspaces… (Jiminny Inc) Has new messages","depth":14,"bounds":{"left":0.27593085,"top":1.0,"width":0.011968086,"height":-0.058260202},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Home","depth":14,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Home","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"DMs","depth":14,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"DMs","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Activity","depth":14,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Activity","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":14,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Later","depth":14,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Later","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"More…","depth":14,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Unreads","depth":21,"bounds":{"left":0.30718085,"top":1.0,"width":0.018949468,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Threads","depth":21,"bounds":{"left":0.30718085,"top":1.0,"width":0.01761968,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Huddles","depth":21,"bounds":{"left":0.30718085,"top":1.0,"width":0.018284574,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drafts & sent","depth":21,"bounds":{"left":0.30718085,"top":1.0,"width":0.02925532,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":21,"bounds":{"left":0.3587101,"top":1.0,"width":0.0026595744,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Directories","depth":21,"bounds":{"left":0.30718085,"top":1.0,"width":0.024268618,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-x-integration-app","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.043882977,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"platform-inner-team","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.04454787,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ai-chapter","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.022273935,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"alerts","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.012300532,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"backend","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.018284574,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"bugs","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.010638298,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"confusion-clinic","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.034574468,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"curiosity_lab","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.027593086,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"engineering","depth":23,"bounds":{"left":0.3125,"top":1.0,"width":0.025930852,"height":-0.09177971},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"general","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-bg","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"platform-tickets","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"product_launches","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"random","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"releases","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"sofia-office","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"support","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"thank-yous","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"the_people_of_jiminny","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Galya Dimitrova","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Vasil Vasilev","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Stefka Stoyanova","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Todor Stamatov","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Mario Georgiev","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"James Graham","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tanev","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Steliyan Georgiev","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Petko Kashinski","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Lukas Kovalik","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"you","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Toast","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jira Cloud","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Google Calendar","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Messages","depth":17,"bounds":{"left":0.3723404,"top":1.0,"width":0.030917553,"height":-0.09177971},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Messages","depth":19,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Add canvas","depth":18,"bounds":{"left":0.40425533,"top":1.0,"width":0.034242023,"height":-0.09177971},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Add canvas","depth":20,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":17,"bounds":{"left":0.43949467,"top":1.0,"width":0.020944148,"height":-0.09177971},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":19,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Pins","depth":17,"bounds":{"left":0.46176863,"top":1.0,"width":0.020279255,"height":-0.09177971},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Pins","depth":19,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Add and Edit Channel Tabs","depth":17,"bounds":{"left":0.48304522,"top":1.0,"width":0.010970744,"height":-0.09177971},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Canvas","depth":17,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"List","depth":17,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Folder","depth":17,"on_screen":false,"role_description":"text"},{"role":"AXPopUpButton","text":"Jump to date","depth":23,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Vasil Vasilev","depth":24,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:35:14 PM","depth":24,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:35 PM","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"наскоро рових по тоя код","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:35:33 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:35","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"махаме lead id и подменяме stage id с това от сделката","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:35:48 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:35","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"въпроса е дали е редно да го правим от историческа гледна точка","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:40:15 PM","depth":24,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:40 PM","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ами тука не знам, като цяло това му беше идея","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:40:29 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:40","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"да знаеш по кое време е направено activity","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:40:47 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:40","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"но щом вече го сменяваме при lead","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:41:51 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:41","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"не е линеарно една сделка или lead която се развива във времето, сега си скаче напред назад както го мачнем","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:42:30 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:42","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ще пиша на Галя да я питам какво да правим","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Vasil Vasilev","depth":24,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:42:41 PM","depth":24,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:42 PM","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"моето е по скоро идея","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:42:54 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:42","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"в момента не го правим, но и почти не го показваме никъде като информация","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:43:10 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:43","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ние имаме някаква история на сделката, имаме стейдж по време на импорта на активитито","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:43:22 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:43","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"но тоя стейдж го ползваме само за това търсене в on demand","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(edited)","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:43:52 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:43","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insights","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:44:03 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:44","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"имаме дата на отваряне и затваряне на сделка","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:44:21 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:44","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"защо да не сложим и един маркер кога се е променил стейджа","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:44:31 PM","depth":25,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-1608129653546005408
|
-1568416150776787632
|
idle
|
hybrid
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
1
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
bugs
confusion-clinic
curiosity_lab
engineering
general
jiminny-bg
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Galya Dimitrova
Vasil Vasilev
Aneliya Angelova
Stefka Stoyanova
Todor Stamatov
Mario Georgiev
Nikolay Ivanov
James Graham
Stoyan Tanev
Steliyan Georgiev
Petko Kashinski
Lukas Kovalik
you
Toast
Jira Cloud
Google Calendar
Messages
Messages
Add canvas
Add canvas
Files
Files
Pins
Pins
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Vasil Vasilev
Today at 2:35:14 PM
2:35 PM
наскоро рових по тоя код
Today at 2:35:33 PM
2:35
махаме lead id и подменяме stage id с това от сделката
Today at 2:35:48 PM
2:35
въпроса е дали е редно да го правим от историческа гледна точка
Lukas Kovalik
Today at 2:40:15 PM
2:40 PM
ами тука не знам, като цяло това му беше идея
Today at 2:40:29 PM
2:40
да знаеш по кое време е направено activity
Today at 2:40:47 PM
2:40
но щом вече го сменяваме при lead
Today at 2:41:51 PM
2:41
не е линеарно една сделка или lead която се развива във времето, сега си скаче напред назад както го мачнем
Today at 2:42:30 PM
2:42
ще пиша на Галя да я питам какво да правим
Vasil Vasilev
Today at 2:42:41 PM
2:42 PM
моето е по скоро идея
Today at 2:42:54 PM
2:42
в момента не го правим, но и почти не го показваме никъде като информация
Today at 2:43:10 PM
2:43
ние имаме някаква история на сделката, имаме стейдж по време на импорта на активитито
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:43:22 PM
2:43
но тоя стейдж го ползваме само за това търсене в on demand
(edited)
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:43:52 PM
2:43
преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insights
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:03 PM
2:44
имаме дата на отваряне и затваряне на сделка
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:21 PM
2:44
защо да не сложим и един маркер кога се е променил стейджа
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:31 PM
PnostormcodeFV faVsco.js#12077 on JY-20903-update_activity-stroledey© TextRelayService.phgC) DeleteForCoachecCom© Sso.php© ValidateSendingMessage.phpDownloaamissing lrack© ActivityRepository.phpc rixAcuivitiesopportunitC) HaraDeleteActivitiestol© HardDeleteActivitiesTe© EventserviceProvider.pnp© ProspectCache.php© StaleRecordValidator.phpc) hyoratecalwincrmba© HydrateDefaultActivitv1C) OpportunityRepository.php© UpdateSingleEntity.phpC) MatchActivityCrmData.php(c) Service.php© Client.pheC) InviteMeetinaBot.php© IterateActivities.phd© MiarateLocationFromC:© MonitorDialerActivities(OpportunitySyncTrait.pho(C) PayloadBuilder.php(c) Confiquration.phpmA1л vC) MonitorMeetingcounte© MonitorMeetingEndCor19 04class ImportActzvityuypes implements shouldQueueC) MonitorMeetinostartco(t) MonitorMeetinaTrait.orC) NotifvNotLodded.oho© NotPlayableEmail.phpC) PreMeetinaNotification© PreMeetingReminder.plc) ProheMediaSeamentselC) RoaccianTranscrintComC) ReindeyRecentActivitie(c) RetrvProspectsummary(C) SetProviderCanabilitiec(e) StatusCount.php© SyncMissingCallDisposi© UpdateActivityElasticS€© UpdateElasticSearch.pl•_ Analvucsw Calendarsyalerml> C Hubspot› IntearationApp> D Traits© AddLavoutEntities.php© AutoloaDelavedCommaC) Backfilll@pportunitvUseiC BullhornCommandAbstC) BullhornPinaCommand.C) BullhornSearchCommarC) BullhornSessioncomma@ CheckActivitvl.cagableC) CleanDunlicateSieldDat@ FullSvncOpportunityColC) LoaActivitiesCommandl@ ManageSvncStrategyC@ MatchOnnortunituAativ(C MiaratoDrovidor nhn© ProcessHubspotObject(e) DuracDelotodOnnortun* Create the event Listener..public function __construct(private readonly ResolveTeamCrmConnection ScrmResolver,private readonly FieldRepository $fieldRepository,private readonly PlaybookCategoryRepository Srepository,1...}* Import the standard Event/Task Type picklist options from the CRMpublic function handle(PlaybookCreated $event): voidSplaybook = Sevent->[URL_WITH_CREDENTIALS] HS_local [jiminny@localhost]& console [PROD]A console (EU]tiò accounts [EU]fii stages (EU]tid teams (EU]ImporbotkecoraingJoo.pnp© Activity.php(C) SixActivitiesOnnortunitv.nhn Xi& console SlAGiNGnamespace Jiminny console commanas Accivicles•use ...181class rixacciv1clesupporcunlcy excenas commano20 0protected ssignature = 'activity:f1x-opportunity {--from=} 1--to=}';public function handle(EmailHelper SemailHelper): int{...}1usadeorivate function detcrmservicecfeam steam: ?Serviceinterface....?1 usageorivate function processParticinantsdServicelnterface ScrmService,Emal Helner SemailHelner.Activity Cactivitv): void iSteam = Sactivity->getTeamSuser = Sactivity->getUserO›Sparticipants = Sactivity->getParticipantsO:foreach (Sparticipants as $participant)if (Sparticipant->getUserId !== null || $participant->getEmailAddress === null) {continue.Sema1LAddress = Sparticipant->qetzma1lAddressO*if (SemailHelper->isCompanyEmail(Steam, SemailAddress)) {continue:trysSopportunitv = $this->find0pportunitvInCrm($crmService. SemailAddress. Suser->qetId@):catch (Throwable Se) 4$this->error( string: 'Could not find opportunity:' . $e->getMessageO):Sopportunity = null.i* (Sonnontunitv === null){iSthis->npsetActivitvinnontunitv/Sactivitv)•continue:• Inu 14 May 10:00.13+0 ..B2^cascadePipedrive SDK EvaluaFixing Activity Stage DImport CRM Activity TTnoughttor 4sSearched opportunity id s'= in aooSearched opportunity idis*=in aoo/Comoonent/DealRisksThought for 2s >Read Service.php #L555-584Fix 4 Implementation ReviewI've implemented Fix 4 in two locations:1. Activitv::uodateActivitvCrmData© (lines 2122-2126)iohoif (Sopportunity && $this->opportunity_id != Sopportunity->id) ‹schis→>opporcunity 10 = sopportunlty→10,I Reset stage id when opportunity changes to avoid stale stage from previous opportunity matchiCthic-sstane id = Cctane?_sid.Potential ssue: In Sstage is null. this willl set stage id to nulll even it the new obportunity has a stage in the database.This could happen if the CRM lookup doesn't return stage informationRecommendation: Consider checkina if the opportunity has a stage id and usina that as a tallback6 php.if (sonnortunitv S6 Sthic-sonnortunitv id I== Sonnortunitv-sid)^Sthis->opportunity id = sopportunity->id:naes to avoid stale stage from previous opportunity matchiSthis->stage_id = $stage?->id ?? Sopportunity->stage_id;However, looking at the context, $stage is derived from the CRM lookup results, so if it's null, the opportunity might nothave stage intormation available. The current behavior is acceptable.2. FixActivities0oportunitv command (lines 123-128)oho.Sactivity->uodatedf'opportunity_id' => $opportunity->getId(),'stageid => Sopportunitv->getstageldorNo issues found - This correctly updates both fields togetherOther locations reviewed:•IntearationAoo/Service,oho (line 560): Sets stage id directlv from opportunitv-stage id when creatina/vodatinafrom lead data - this is correct.A TOUrINTEJed weekly usage quota is exhausted. Purchase extra usage to continue using premium models. Quota resets May 17, 11:00Lets have÷ @Code SWF-1.6C0 1]Po. 4 spac...
|
42825
|
NULL
|
NULL
|
NULL
|
|
42828
|
1569
|
0
|
2026-05-14T12:06:46.007581+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760406007_m1.jpg...
|
Slack
|
Vasil Vasilev (DM) - Jiminny Inc - 4 new items - S Vasil Vasilev (DM) - Jiminny Inc - 4 new items - Slack...
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
1
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
bugs
confusion-clinic
curiosity_lab
engineering
general
jiminny-bg
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Galya Dimitrova
Vasil Vasilev
Aneliya Angelova
Stefka Stoyanova
Todor Stamatov
Mario Georgiev
Nikolay Ivanov
James Graham
Stoyan Tanev
Steliyan Georgiev
Petko Kashinski
Lukas Kovalik
you
Toast
Jira Cloud
Google Calendar
Messages
Messages
Add canvas
Add canvas
Files
Files
Pins
Pins
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Vasil Vasilev
Today at 2:35:14 PM
2:35 PM
наскоро рових по тоя код
Today at 2:35:33 PM
2:35
махаме lead id и подменяме stage id с това от сделката
Today at 2:35:48 PM
2:35
въпроса е дали е редно да го правим от историческа гледна точка
Lukas Kovalik
Today at 2:40:15 PM
2:40 PM
ами тука не знам, като цяло това му беше идея
Today at 2:40:29 PM
2:40
да знаеш по кое време е направено activity
Today at 2:40:47 PM
2:40
но щом вече го сменяваме при lead
Today at 2:41:51 PM
2:41
не е линеарно една сделка или lead която се развива във времето, сега си скаче напред назад както го мачнем
Today at 2:42:30 PM
2:42
ще пиша на Галя да я питам какво да правим
Vasil Vasilev
Today at 2:42:41 PM
2:42 PM
моето е по скоро идея
Today at 2:42:54 PM
2:42
в момента не го правим, но и почти не го показваме никъде като информация
Today at 2:43:10 PM
2:43
ние имаме някаква история на сделката, имаме стейдж по време на импорта на активитито
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:43:22 PM
2:43
но тоя стейдж го ползваме само за това търсене в on demand
(edited)
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:43:52 PM
2:43
преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insights
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:03 PM
2:44
имаме дата на отваряне и затваряне на сделка
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:21 PM
2:44
защо да не сложим и един маркер кога се е променил стейджа
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:31 PM
2:44
така в таймлайна ще се вижда “развитието” на тая сделка
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 2:45:09 PM
2:45 PM
хм ами то дали да го няма вече
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Vasil Vasilev
Today at 2:45:19 PM
2:45 PM
няма го
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 2:46:46 PM
2:46 PM
няма да
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:46:56 PM
2:46
но BE си го връща
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:47:01 PM
2:47
с тази цел точно
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Vasil Vasilev
Today at 2:47:07 PM
2:47 PM
знам, проверих, преди да предложа на Галя да ги покажем
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:47:09 PM
2:47
възможно
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:47:11 PM
2:47
но явно не е направено
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Vasil Vasilev
Today at 3:04:09 PM
3:04 PM
в крайна сметка тоя метод Activity::updateActivityCrmData се вика само при подмяна на opportunity, нали така?
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 3:05:52 PM
3:05 PM
гледам го сега
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Conversation marked as unread from this message
Vasil Vasilev is typing...
|
[{"role":"AXPopUpButton","text [{"role":"AXPopUpButton","text":"Switch workspaces… (Jiminny Inc) Has new messages","depth":14,"bounds":{"left":0.011805556,"top":0.08111111,"width":0.025,"height":0.04},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Home","depth":14,"bounds":{"left":0.00625,"top":0.14,"width":0.036111113,"height":0.075555556},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Home","depth":16,"bounds":{"left":0.013888889,"top":0.19222222,"width":0.020833334,"height":0.015555556},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"DMs","depth":14,"bounds":{"left":0.00625,"top":0.21555555,"width":0.036111113,"height":0.075555556},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"DMs","depth":16,"bounds":{"left":0.015972223,"top":0.26777777,"width":0.016666668,"height":0.015555556},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Activity","depth":14,"bounds":{"left":0.00625,"top":0.2911111,"width":0.036111113,"height":0.075555556},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Activity","depth":16,"bounds":{"left":0.011111111,"top":0.34333333,"width":0.027083334,"height":0.015555556},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.011111111,"top":0.34333333,"width":0.0055555557,"height":0.015555556}},{"char_start":1,"char_count":7,"bounds":{"left":0.015972223,"top":0.34333333,"width":0.022222223,"height":0.015555556}}],"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":14,"bounds":{"left":0.00625,"top":0.36666667,"width":0.036111113,"height":0.075555556},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":16,"bounds":{"left":0.016666668,"top":0.4188889,"width":0.015972223,"height":0.015555556},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.016666668,"top":0.4188889,"width":0.004166667,"height":0.015555556}},{"char_start":1,"char_count":4,"bounds":{"left":0.020833334,"top":0.4188889,"width":0.011805556,"height":0.015555556}}],"role_description":"text"},{"role":"AXRadioButton","text":"Later","depth":14,"bounds":{"left":0.00625,"top":0.4422222,"width":0.036111113,"height":0.075555556},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Later","depth":16,"bounds":{"left":0.015277778,"top":0.49444443,"width":0.018055556,"height":0.015555556},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"More…","depth":14,"bounds":{"left":0.00625,"top":0.5177778,"width":0.036111113,"height":0.075555556},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More","depth":16,"bounds":{"left":0.015277778,"top":0.57,"width":0.01875,"height":0.015555556},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Unreads","depth":21,"bounds":{"left":0.077083334,"top":0.12777779,"width":0.039583333,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Threads","depth":21,"bounds":{"left":0.077083334,"top":0.12777779,"width":0.036805555,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Huddles","depth":21,"bounds":{"left":0.077083334,"top":0.12777779,"width":0.038194444,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drafts & sent","depth":21,"bounds":{"left":0.077083334,"top":0.12777779,"width":0.06111111,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":21,"bounds":{"left":0.18472221,"top":0.12777779,"width":0.0055555557,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Directories","depth":21,"bounds":{"left":0.077083334,"top":0.12777779,"width":0.050694443,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-x-integration-app","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.09166667,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"platform-inner-team","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.093055554,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ai-chapter","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.046527777,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"alerts","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.025694445,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"backend","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.038194444,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"bugs","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.022222223,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"confusion-clinic","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.072222225,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"curiosity_lab","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.057638887,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"engineering","depth":23,"bounds":{"left":0.088194445,"top":0.12777779,"width":0.054166667,"height":0.012222222},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"general","depth":23,"bounds":{"left":0.088194445,"top":0.15111111,"width":0.034027778,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-bg","depth":23,"bounds":{"left":0.088194445,"top":0.18222222,"width":0.048611112,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"platform-tickets","depth":23,"bounds":{"left":0.088194445,"top":0.21333334,"width":0.072916664,"height":0.02},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.088194445,"top":0.21333334,"width":0.00625,"height":0.02}},{"char_start":1,"char_count":15,"bounds":{"left":0.094444446,"top":0.21333334,"width":0.06666667,"height":0.02}}],"role_description":"text"},{"role":"AXStaticText","text":"product_launches","depth":23,"bounds":{"left":0.088194445,"top":0.24444444,"width":0.08055556,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"random","depth":23,"bounds":{"left":0.088194445,"top":0.27555555,"width":0.035416666,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"releases","depth":23,"bounds":{"left":0.088194445,"top":0.30666667,"width":0.038194444,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"sofia-office","depth":23,"bounds":{"left":0.088194445,"top":0.33777776,"width":0.05138889,"height":0.02},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.088194445,"top":0.33777776,"width":0.0048611113,"height":0.02}},{"char_start":1,"char_count":11,"bounds":{"left":0.093055554,"top":0.33777776,"width":0.045833334,"height":0.02}}],"role_description":"text"},{"role":"AXStaticText","text":"support","depth":23,"bounds":{"left":0.088194445,"top":0.36888888,"width":0.036111113,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"thank-yous","depth":23,"bounds":{"left":0.088194445,"top":0.4,"width":0.05138889,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"the_people_of_jiminny","depth":23,"bounds":{"left":0.088194445,"top":0.4311111,"width":0.094444446,"height":0.02},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.088194445,"top":0.4311111,"width":0.004166667,"height":0.02}},{"char_start":1,"char_count":20,"bounds":{"left":0.09236111,"top":0.4311111,"width":0.09861111,"height":0.02}}],"role_description":"text"},{"role":"AXStaticText","text":"Galya Dimitrova","depth":23,"bounds":{"left":0.088194445,"top":0.5044444,"width":0.06388889,"height":0.02},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.088194445,"top":0.5044444,"width":0.007638889,"height":0.02}},{"char_start":1,"char_count":14,"bounds":{"left":0.09583333,"top":0.5044444,"width":0.068055555,"height":0.02}}],"role_description":"text"},{"role":"AXStaticText","text":"Vasil Vasilev","depth":23,"bounds":{"left":0.088194445,"top":0.53555554,"width":0.055555556,"height":0.02},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.088194445,"top":0.53555554,"width":0.00625,"height":0.02}},{"char_start":1,"char_count":12,"bounds":{"left":0.094444446,"top":0.53555554,"width":0.048611112,"height":0.02}}],"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"bounds":{"left":0.088194445,"top":0.56666666,"width":0.07847222,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Stefka Stoyanova","depth":23,"bounds":{"left":0.088194445,"top":0.5977778,"width":0.079166666,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Todor Stamatov","depth":23,"bounds":{"left":0.088194445,"top":0.6288889,"width":0.072222225,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Mario Georgiev","depth":23,"bounds":{"left":0.088194445,"top":0.66,"width":0.07152778,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":23,"bounds":{"left":0.088194445,"top":0.6911111,"width":0.06736111,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"James Graham","depth":23,"bounds":{"left":0.088194445,"top":0.7222222,"width":0.06666667,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tanev","depth":23,"bounds":{"left":0.088194445,"top":0.75333333,"width":0.060416665,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Steliyan Georgiev","depth":23,"bounds":{"left":0.088194445,"top":0.78444445,"width":0.07986111,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Petko Kashinski","depth":23,"bounds":{"left":0.088194445,"top":0.8155556,"width":0.072222225,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Lukas Kovalik","depth":23,"bounds":{"left":0.088194445,"top":0.8466667,"width":0.061805554,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"you","depth":23,"bounds":{"left":0.15555556,"top":0.8466667,"width":0.013194445,"height":0.02},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.15555556,"top":0.8466667,"width":0.0048611113,"height":0.02}},{"char_start":1,"char_count":2,"bounds":{"left":0.16041666,"top":0.8466667,"width":0.011805556,"height":0.02}}],"role_description":"text"},{"role":"AXStaticText","text":"Toast","depth":23,"bounds":{"left":0.088194445,"top":0.92,"width":0.025694445,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jira Cloud","depth":23,"bounds":{"left":0.088194445,"top":0.95111114,"width":0.045833334,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Google Calendar","depth":23,"bounds":{"left":0.088194445,"top":0.9822222,"width":0.06388889,"height":0.012222222},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Messages","depth":17,"bounds":{"left":0.21319444,"top":0.12777779,"width":0.06458333,"height":0.04222222},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Messages","depth":19,"bounds":{"left":0.2326389,"top":0.14,"width":0.039583333,"height":0.017777778},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Add canvas","depth":18,"bounds":{"left":0.27986112,"top":0.12777779,"width":0.07152778,"height":0.04222222},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Add canvas","depth":20,"bounds":{"left":0.29930556,"top":0.14,"width":0.046527777,"height":0.017777778},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":17,"bounds":{"left":0.35347223,"top":0.12777779,"width":0.04375,"height":0.04222222},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":19,"bounds":{"left":0.37291667,"top":0.14,"width":0.01875,"height":0.017777778},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.37291667,"top":0.14,"width":0.0055555557,"height":0.017777778}},{"char_start":1,"char_count":4,"bounds":{"left":0.3784722,"top":0.14,"width":0.013194445,"height":0.017777778}}],"role_description":"text"},{"role":"AXRadioButton","text":"Pins","depth":17,"bounds":{"left":0.4,"top":0.12777779,"width":0.04236111,"height":0.04222222},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Pins","depth":19,"bounds":{"left":0.41944444,"top":0.14,"width":0.017361112,"height":0.017777778},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Add and Edit Channel Tabs","depth":17,"bounds":{"left":0.44444445,"top":0.12777779,"width":0.022916667,"height":0.04222222},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Canvas","depth":17,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"List","depth":17,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Folder","depth":17,"on_screen":false,"role_description":"text"},{"role":"AXPopUpButton","text":"Jump to date","depth":23,"bounds":{"left":0.5729167,"top":0.17666666,"width":0.05277778,"height":0.031111112},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Vasil Vasilev","depth":24,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.057638887,"height":0.0011111111},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.30416667,"top":0.16111112,"width":0.0055555557,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:35:14 PM","depth":24,"bounds":{"left":0.3090278,"top":0.16111112,"width":0.031944446,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:35 PM","depth":25,"bounds":{"left":0.3090278,"top":0.16111112,"width":0.031944446,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"наскоро рових по тоя код","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.12291667,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:35:33 PM","depth":25,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:35","depth":26,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"махаме lead id и подменяме stage id с това от сделката","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.26666668,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:35:48 PM","depth":25,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:35","depth":26,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"въпроса е дали е редно да го правим от историческа гледна точка","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.32083333,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.06458333,"height":0.0011111111},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.31111112,"top":0.16111112,"width":0.0055555557,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:40:15 PM","depth":24,"bounds":{"left":0.31666666,"top":0.16111112,"width":0.03125,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:40 PM","depth":25,"bounds":{"left":0.31666666,"top":0.16111112,"width":0.03125,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ами тука не знам, като цяло това му беше идея","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.22847222,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:40:29 PM","depth":25,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:40","depth":26,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"да знаеш по кое време е направено activity","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.20763889,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:40:47 PM","depth":25,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:40","depth":26,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"но щом вече го сменяваме при lead","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.1701389,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:41:51 PM","depth":25,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:41","depth":26,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"не е линеарно една сделка или lead която се развива във времето, сега си скаче напред назад както го мачнем","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.53402776,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:42:30 PM","depth":25,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:42","depth":26,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ще пиша на Галя да я питам какво да правим","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.21597221,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Vasil Vasilev","depth":24,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.057638887,"height":0.0011111111},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.30416667,"top":0.16111112,"width":0.0055555557,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:42:41 PM","depth":24,"bounds":{"left":0.3090278,"top":0.16111112,"width":0.031944446,"height":0.0011111111},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:42 PM","depth":25,"bounds":{"left":0.3090278,"top":0.16111112,"width":0.031944446,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"моето е по скоро идея","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.10902778,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:42:54 PM","depth":25,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.013333334},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:42","depth":26,"bounds":{"left":0.22430556,"top":0.16111112,"width":0.016666668,"height":0.013333334},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"в момента не го правим, но и почти не го показваме никъде като информация","depth":25,"bounds":{"left":0.24652778,"top":0.16111112,"width":0.37708333,"height":0.013333334},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:43:10 PM","depth":25,"bounds":{"left":0.22430556,"top":0.19111112,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:43","depth":26,"bounds":{"left":0.22430556,"top":0.19111112,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ние имаме някаква история на сделката, имаме стейдж по време на импорта на активитито","depth":25,"bounds":{"left":0.24652778,"top":0.18777777,"width":0.44444445,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:43:22 PM","depth":25,"bounds":{"left":0.22430556,"top":0.22444445,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:43","depth":26,"bounds":{"left":0.22430556,"top":0.22444445,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"но тоя стейдж го ползваме само за това търсене в on demand","depth":25,"bounds":{"left":0.24652778,"top":0.2211111,"width":0.29652777,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"bounds":{"left":0.54236114,"top":0.22333333,"width":0.0027777778,"height":0.017777778},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(edited)","depth":25,"bounds":{"left":0.5451389,"top":0.22333333,"width":0.029861111,"height":0.017777778},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.5451389,"top":0.22333333,"width":0.0020833334,"height":0.017777778}},{"char_start":1,"char_count":7,"bounds":{"left":0.5472222,"top":0.22333333,"width":0.027777778,"height":0.017777778}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"bounds":{"left":0.57430553,"top":0.22333333,"width":0.0027777778,"height":0.017777778},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:43:52 PM","depth":25,"bounds":{"left":0.22430556,"top":0.25777778,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:43","depth":26,"bounds":{"left":0.22430556,"top":0.25777778,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insights","depth":25,"bounds":{"left":0.24652778,"top":0.25444445,"width":0.59166664,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:44:03 PM","depth":25,"bounds":{"left":0.22430556,"top":0.2911111,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:44","depth":26,"bounds":{"left":0.22430556,"top":0.2911111,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"имаме дата на отваряне и затваряне на сделка","depth":25,"bounds":{"left":0.24652778,"top":0.28777778,"width":0.225,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:44:21 PM","depth":25,"bounds":{"left":0.22430556,"top":0.32444444,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:44","depth":26,"bounds":{"left":0.22430556,"top":0.32444444,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"защо да не сложим и един маркер кога се е променил стейджа","depth":25,"bounds":{"left":0.24652778,"top":0.3211111,"width":0.30625,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:44:31 PM","depth":25,"bounds":{"left":0.22430556,"top":0.35777777,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:44","depth":26,"bounds":{"left":0.22430556,"top":0.35777777,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"така в таймлайна ще се вижда “развитието” на тая сделка","depth":25,"bounds":{"left":0.24652778,"top":0.35444444,"width":0.27986112,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"bounds":{"left":0.24652778,"top":0.38555557,"width":0.06458333,"height":0.024444444},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.31111112,"top":0.38777778,"width":0.0055555557,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:45:09 PM","depth":24,"bounds":{"left":0.31666666,"top":0.3911111,"width":0.03125,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:45 PM","depth":25,"bounds":{"left":0.31666666,"top":0.3911111,"width":0.03125,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"хм ами то дали да го няма вече","depth":25,"bounds":{"left":0.24652778,"top":0.41222224,"width":0.15138888,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Vasil Vasilev","depth":24,"bounds":{"left":0.24652778,"top":0.44333333,"width":0.057638887,"height":0.024444444},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.30416667,"top":0.44555557,"width":0.0055555557,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:45:19 PM","depth":24,"bounds":{"left":0.3090278,"top":0.4488889,"width":0.031944446,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:45 PM","depth":25,"bounds":{"left":0.3090278,"top":0.4488889,"width":0.031944446,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"няма го","depth":25,"bounds":{"left":0.24652778,"top":0.47,"width":0.036805555,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"bounds":{"left":0.24652778,"top":0.5011111,"width":0.06458333,"height":0.024444444},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.31111112,"top":0.50333333,"width":0.0055555557,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:46:46 PM","depth":24,"bounds":{"left":0.31666666,"top":0.50666666,"width":0.03125,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:46 PM","depth":25,"bounds":{"left":0.31666666,"top":0.50666666,"width":0.03125,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"няма да","depth":25,"bounds":{"left":0.24652778,"top":0.5277778,"width":0.038194444,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:46:56 PM","depth":25,"bounds":{"left":0.22430556,"top":0.5644444,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:46","depth":26,"bounds":{"left":0.22430556,"top":0.5644444,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"но BE си го връща","depth":25,"bounds":{"left":0.24652778,"top":0.5611111,"width":0.0875,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:47:01 PM","depth":25,"bounds":{"left":0.22430556,"top":0.5977778,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:47","depth":26,"bounds":{"left":0.22430556,"top":0.5977778,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"с тази цел точно","depth":25,"bounds":{"left":0.24652778,"top":0.59444445,"width":0.08055556,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Vasil Vasilev","depth":24,"bounds":{"left":0.24652778,"top":0.6255556,"width":0.057638887,"height":0.024444444},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.30416667,"top":0.62777776,"width":0.0055555557,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:47:07 PM","depth":24,"bounds":{"left":0.3090278,"top":0.6311111,"width":0.031944446,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:47 PM","depth":25,"bounds":{"left":0.3090278,"top":0.6311111,"width":0.031944446,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"знам, проверих, преди да предложа на Галя да ги покажем","depth":25,"bounds":{"left":0.24652778,"top":0.6522222,"width":0.28402779,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:47:09 PM","depth":25,"bounds":{"left":0.22430556,"top":0.6888889,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:47","depth":26,"bounds":{"left":0.22430556,"top":0.6888889,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"възможно","depth":25,"bounds":{"left":0.24652778,"top":0.6855556,"width":0.049305554,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Today at 2:47:11 PM","depth":25,"bounds":{"left":0.22430556,"top":0.7222222,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:47","depth":26,"bounds":{"left":0.22430556,"top":0.7222222,"width":0.016666668,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"но явно не е направено","depth":25,"bounds":{"left":0.24652778,"top":0.7188889,"width":0.11180556,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Vasil Vasilev","depth":24,"bounds":{"left":0.24652778,"top":0.75,"width":0.057638887,"height":0.024444444},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.30416667,"top":0.75222224,"width":0.0055555557,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 3:04:09 PM","depth":24,"bounds":{"left":0.3090278,"top":0.75555557,"width":0.031944446,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3:04 PM","depth":25,"bounds":{"left":0.3090278,"top":0.75555557,"width":0.031944446,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"в крайна сметка тоя метод Activity::updateActivityCrmData се вика само при подмяна на opportunity, нали така?","depth":25,"bounds":{"left":0.24652778,"top":0.77666664,"width":0.5347222,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"bounds":{"left":0.24652778,"top":0.80777776,"width":0.06458333,"height":0.024444444},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.31111112,"top":0.81,"width":0.0055555557,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 3:05:52 PM","depth":24,"bounds":{"left":0.31666666,"top":0.81333333,"width":0.03125,"height":0.016666668},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3:05 PM","depth":25,"bounds":{"left":0.31666666,"top":0.81333333,"width":0.03125,"height":0.016666668},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"гледам го сега","depth":25,"bounds":{"left":0.24652778,"top":0.83444446,"width":0.06944445,"height":0.02},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"","depth":23,"bounds":{"left":0.21666667,"top":0.88,"width":0.7652778,"height":0.04222222},"on_screen":true,"value":"","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Conversation marked as unread from this message","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Vasil Vasilev is typing","depth":11,"on_screen":false,"role_description":"text"}]...
|
-1177022757992950536
|
-1573069284087248560
|
idle
|
hybrid
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
1
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
bugs
confusion-clinic
curiosity_lab
engineering
general
jiminny-bg
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Galya Dimitrova
Vasil Vasilev
Aneliya Angelova
Stefka Stoyanova
Todor Stamatov
Mario Georgiev
Nikolay Ivanov
James Graham
Stoyan Tanev
Steliyan Georgiev
Petko Kashinski
Lukas Kovalik
you
Toast
Jira Cloud
Google Calendar
Messages
Messages
Add canvas
Add canvas
Files
Files
Pins
Pins
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Vasil Vasilev
Today at 2:35:14 PM
2:35 PM
наскоро рових по тоя код
Today at 2:35:33 PM
2:35
махаме lead id и подменяме stage id с това от сделката
Today at 2:35:48 PM
2:35
въпроса е дали е редно да го правим от историческа гледна точка
Lukas Kovalik
Today at 2:40:15 PM
2:40 PM
ами тука не знам, като цяло това му беше идея
Today at 2:40:29 PM
2:40
да знаеш по кое време е направено activity
Today at 2:40:47 PM
2:40
но щом вече го сменяваме при lead
Today at 2:41:51 PM
2:41
не е линеарно една сделка или lead която се развива във времето, сега си скаче напред назад както го мачнем
Today at 2:42:30 PM
2:42
ще пиша на Галя да я питам какво да правим
Vasil Vasilev
Today at 2:42:41 PM
2:42 PM
моето е по скоро идея
Today at 2:42:54 PM
2:42
в момента не го правим, но и почти не го показваме никъде като информация
Today at 2:43:10 PM
2:43
ние имаме някаква история на сделката, имаме стейдж по време на импорта на активитито
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:43:22 PM
2:43
но тоя стейдж го ползваме само за това търсене в on demand
(edited)
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:43:52 PM
2:43
преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insights
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:03 PM
2:44
имаме дата на отваряне и затваряне на сделка
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:21 PM
2:44
защо да не сложим и един маркер кога се е променил стейджа
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:44:31 PM
2:44
така в таймлайна ще се вижда “развитието” на тая сделка
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 2:45:09 PM
2:45 PM
хм ами то дали да го няма вече
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Vasil Vasilev
Today at 2:45:19 PM
2:45 PM
няма го
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 2:46:46 PM
2:46 PM
няма да
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:46:56 PM
2:46
но BE си го връща
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:47:01 PM
2:47
с тази цел точно
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Vasil Vasilev
Today at 2:47:07 PM
2:47 PM
знам, проверих, преди да предложа на Галя да ги покажем
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:47:09 PM
2:47
възможно
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Today at 2:47:11 PM
2:47
но явно не е направено
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Vasil Vasilev
Today at 3:04:09 PM
3:04 PM
в крайна сметка тоя метод Activity::updateActivityCrmData се вика само при подмяна на opportunity, нали така?
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Lukas Kovalik
Today at 3:05:52 PM
3:05 PM
гледам го сега
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Conversation marked as unread from this message
Vasil Vasilev is typing
50HomeDMsActivityFilesLater..•MoreSlackFileEditViewEDJiminny ...# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...Direct messages. Galya Dimit.... Vasil VasilevP. Aneliya AngelovaStefka Stoyanova: Todor StamatovMario GeorgievNikolay Ivanovdo James Graham. Stoyan TanevSteliyan GeorgievPetko KashinskiLukas Kovalik y...:: AppsToastJira CloudConala CalaGoHistoryWindowHelp< →0 ahlSupport Daily • nowQDescribe what you are looking forVasil VasilevSupport Dailynow - 15:00-15:15= Notes - Support Daily.MessagesAdd canvasO Files& Pins +ние имаме някаква история на сделката, имаме стейдж по време наTodayна активититоно тоя стейдж го ползваме само за това тьрсене в on demand (edited)преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insightsимаме дата на отваряне и затваряне на сделказащо да не сложим и един маркер кога се е променил стейджатака в таймлайна ще се вижда "развитието" на тая сделкаLukas Kovalik 2:45 PMхм ами то дали да го няма вечеVasil Vasilev 2:45 PMняма гоLukas Kovalik 2:46 PMняма дано ВЕ си го врьщас тази цел точноVasil Vasilev 2:47 PMзнам, проверих, преди да предложа на Галя да ги покажемвЪЗМоЖНоно явно не е направеноVasil Vasilev 3:04 PMв крайна сметка тоя метод Activity:updateActivityCrmData се вика само при подмяна на opportunity, нали така?Lukas Kovalik 3:05 PMгледам го сегаMessage Vasil Vasilev+Аа100% ( 8• Thu 14 May 15:06:45C Join Google Meet...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
42830
|
1570
|
0
|
2026-05-14T12:06:46.720996+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760406720_m2.jpg...
|
PhpStorm
|
faVsco.js – Activity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PnostormFV faVsco.js#12077 on JY-20903-update_acti PnostormFV faVsco.js#12077 on JY-20903-update_activity-stroledey© TextRelayService.phgC) DeleteForCoachecCom© Sso.php© ValidateSendingMessage.phpDownloaamissing lrack© ActivityRepository.phpc rixAcuivitiesopportunitC) HaraDeleteActivitiestol© HardDeleteActivitiesTe© EventserviceProvider.pnp© ProspectCache.php© StaleRecordValidator.phpc) hyoratecalwincrmba© HydrateDefaultActivitv1C) OpportunityRepository.php© UpdateSingleEntity.phpC) MatchActivityCrmData.php(c) Service.php© Client.pheC) InviteMeetinaBot.php© IterateActivities.phd© MiarateLocationFromC:© MonitorDialerActivities(OpportunitySynctrait.pho(C) PayloadBuilder.php(c) Confiquration.phpmA1л vC) MonitorMeetingcounte© MonitorMeetingEndCor19 04class ImportActzvityuypes implements shouldQueueC) MonitorMeetinostartco(t) MonitorMeetinaTrait.orC) NotifvNotLodded.oho© NotPlayableEmail.phpC) PreMeetinaNotification© PreMeetingReminder.plc) ProheMediaSeamentselC) RoaccianTranscrintComC) ReindeyRecentActivitie(c) RetrvProspectsummary(C) SetProviderCanabilitiec(e) StatusCount.php© SyncMissingCallDisposi© UpdateActivityElasticS€© UpdateElasticSearch.pl•_ Analvucsw Calendarsyalerml> C Hubspot› IntearationApp> C Traits© AddLavoutEntities.php© AutoloaDelavedCommaC) Backfilll@pportunitvUseiC BullhornCommandAbstC) BullhornPinaCommand.C) BullhornSearchCommarC) BullhornSessioncomma@ CheckActivitvl.cagableC) CleanDunlicateSieldDat@ FullSvncOpportunityColC) LoaActivitiesCommandl@ ManageSvncStrategyC@ MatchOnnortunituAativ(C MiaratoDrovidor nhn© ProcessHubspotObject(e) DuracDelotodOnnortun* Create the event Listener..public function __construct(private readonly ResolveTeamCrmConnection ScrmResolver,private readonly FieldRepository $fieldRepository,private readonly PlaybookCategoryRepository Srepository,1...}* Import the standard Event/Task Type picklist options from the CRMpublic function handle(PlaybookCreated $event): voidSplaybook = Sevent->[URL_WITH_CREDENTIALS] HS_local [jiminny@localhost]console [PRODIA console (EU]tiò accounts [EU]fii stages (EU]tid teams (EU]ImporbotkecoraingJoo.org© Activity.php x(C) SixActivitiesOnnortunitv.nhn Xi& console SlAGiNGnamespace Jiminny console commanas Accivicles•use ...181class rixacciv1clesupporcunlcy excenas commano20 0protected ssignature = 'activity:f1x-opportunity {--from=} 1--to=}';public function handle(EmailHelper SemailHelper): int{...}1usadeorivate function detcrmservicecfeam steam: ?Serviceinterface.....1 usageorivate function processParticinantsdServicelnterface ScrmService,Emal Helner SemailHelner.Activity Cactivitv): void iSteam = Sactivity->getTeamSuser = Sactivity->getUserOSparticipants = Sactivity->getParticipantsO:foreach (Sparticipants as $participant)if (Sparticipant->getUserId !== null || $participant->getEmailAddress === null) {continue.Sema1LAddress = Sparticipant->qetzma1lAddressO*if (SemailHelper->isCompanyEmail(Steam, SemailAddress)) {continue:trysSopportunitv = $this->find0pportunitvInCrm($crmService. SemailAddress. Suser->qetId@):catch (Throwable Se) 4$this->error( string: 'Could not find opportunity:' . $e->getMessageO):Sopportunity = null.i* Sonnontunitv === nul){iSthis->npsetActivitvinnontunitv/Sactivitv)•continue:• Inu 14 May 10:00.40+0 ..B2^Pipedrive SDK EvaluaFixing Activity Stage DImport CRM Activity TI've Implemented Fix 4 in two locations:1. Activ1ty::updateAct1v1tycrmbata) Unes 2122-2126phoif (Sopportunity && Sthis->opportunity_id !== Sopportunity→>id) ‹$this->opportunity_id = $opportunity->id;Reset stage 1d when opportunity changes to avold stale stage trom previous opportunity matchsthis->stage 1d = Sstage7->1d:Potential Issue: If Sstage is null, this will set stage id to null even if the new opportunity has a stage in the databaseThis could hannen if the CRM lookun doesn't return stage informationRecommendation: Consider checking if the opportunity has a stage id and using that as a fallbackh ohoiSopportunity d Sthis->ooportunity 1d == Sooportunity->id)Sthis->opportunity id = Sopportunity→>id;nes to avoid stale stage from previous opportunity matchSthis-sstage id = Sstage?->id 22 Sonportunitv->stage id:However. lookina at the context. Sstage Is derived trom the CR lookup results, so ifit's null, the opportunity micht notnave stage intormation avallable. Une current denavior is acceptable.2. FixActivitiesOpportunity command (lines 123-128)"phoSactivity->updatedopportunity_id' => Sopportunity->getid(),Stade10 = sopportunzty-oerstadesdnNo issues found - This correctlv undates both fields toaethenOther locatione reviewed.•IntegrationApp/Service.php (line 560): Sets stage id directly from opportunity→>stage id when creating/updatingfrom lead data - this is correct• Copy command: Copies both opportunity id and stage_id together - this is correct.ActivitvController: Handles ctaae id from ucer reauects - not related to the bualTest Results• Uodatecrmbatatest : 2 tests passed (6 assertions)• MatchGrmObiectTect : 15 tocts nasced (75 accertions))The implementation is sound. The only minor concern is the null stage case in updateActivityCrmData(), but this isaccentable aiven the conteyt.Toa1l "• Your included weekly usage quota is exhausted. Purchase extra usage to continue using premium models. Quota resets May 17, 11:00Lets have÷ @Code SWF-1.6W Windsurf TeamoPo. 4 spa...
|
NULL
|
799191147947492202
|
NULL
|
click
|
ocr
|
NULL
|
PnostormFV faVsco.js#12077 on JY-20903-update_acti PnostormFV faVsco.js#12077 on JY-20903-update_activity-stroledey© TextRelayService.phgC) DeleteForCoachecCom© Sso.php© ValidateSendingMessage.phpDownloaamissing lrack© ActivityRepository.phpc rixAcuivitiesopportunitC) HaraDeleteActivitiestol© HardDeleteActivitiesTe© EventserviceProvider.pnp© ProspectCache.php© StaleRecordValidator.phpc) hyoratecalwincrmba© HydrateDefaultActivitv1C) OpportunityRepository.php© UpdateSingleEntity.phpC) MatchActivityCrmData.php(c) Service.php© Client.pheC) InviteMeetinaBot.php© IterateActivities.phd© MiarateLocationFromC:© MonitorDialerActivities(OpportunitySynctrait.pho(C) PayloadBuilder.php(c) Confiquration.phpmA1л vC) MonitorMeetingcounte© MonitorMeetingEndCor19 04class ImportActzvityuypes implements shouldQueueC) MonitorMeetinostartco(t) MonitorMeetinaTrait.orC) NotifvNotLodded.oho© NotPlayableEmail.phpC) PreMeetinaNotification© PreMeetingReminder.plc) ProheMediaSeamentselC) RoaccianTranscrintComC) ReindeyRecentActivitie(c) RetrvProspectsummary(C) SetProviderCanabilitiec(e) StatusCount.php© SyncMissingCallDisposi© UpdateActivityElasticS€© UpdateElasticSearch.pl•_ Analvucsw Calendarsyalerml> C Hubspot› IntearationApp> C Traits© AddLavoutEntities.php© AutoloaDelavedCommaC) Backfilll@pportunitvUseiC BullhornCommandAbstC) BullhornPinaCommand.C) BullhornSearchCommarC) BullhornSessioncomma@ CheckActivitvl.cagableC) CleanDunlicateSieldDat@ FullSvncOpportunityColC) LoaActivitiesCommandl@ ManageSvncStrategyC@ MatchOnnortunituAativ(C MiaratoDrovidor nhn© ProcessHubspotObject(e) DuracDelotodOnnortun* Create the event Listener..public function __construct(private readonly ResolveTeamCrmConnection ScrmResolver,private readonly FieldRepository $fieldRepository,private readonly PlaybookCategoryRepository Srepository,1...}* Import the standard Event/Task Type picklist options from the CRMpublic function handle(PlaybookCreated $event): voidSplaybook = Sevent->[URL_WITH_CREDENTIALS] HS_local [jiminny@localhost]console [PRODIA console (EU]tiò accounts [EU]fii stages (EU]tid teams (EU]ImporbotkecoraingJoo.org© Activity.php x(C) SixActivitiesOnnortunitv.nhn Xi& console SlAGiNGnamespace Jiminny console commanas Accivicles•use ...181class rixacciv1clesupporcunlcy excenas commano20 0protected ssignature = 'activity:f1x-opportunity {--from=} 1--to=}';public function handle(EmailHelper SemailHelper): int{...}1usadeorivate function detcrmservicecfeam steam: ?Serviceinterface.....1 usageorivate function processParticinantsdServicelnterface ScrmService,Emal Helner SemailHelner.Activity Cactivitv): void iSteam = Sactivity->getTeamSuser = Sactivity->getUserOSparticipants = Sactivity->getParticipantsO:foreach (Sparticipants as $participant)if (Sparticipant->getUserId !== null || $participant->getEmailAddress === null) {continue.Sema1LAddress = Sparticipant->qetzma1lAddressO*if (SemailHelper->isCompanyEmail(Steam, SemailAddress)) {continue:trysSopportunitv = $this->find0pportunitvInCrm($crmService. SemailAddress. Suser->qetId@):catch (Throwable Se) 4$this->error( string: 'Could not find opportunity:' . $e->getMessageO):Sopportunity = null.i* Sonnontunitv === nul){iSthis->npsetActivitvinnontunitv/Sactivitv)•continue:• Inu 14 May 10:00.40+0 ..B2^Pipedrive SDK EvaluaFixing Activity Stage DImport CRM Activity TI've Implemented Fix 4 in two locations:1. Activ1ty::updateAct1v1tycrmbata) Unes 2122-2126phoif (Sopportunity && Sthis->opportunity_id !== Sopportunity→>id) ‹$this->opportunity_id = $opportunity->id;Reset stage 1d when opportunity changes to avold stale stage trom previous opportunity matchsthis->stage 1d = Sstage7->1d:Potential Issue: If Sstage is null, this will set stage id to null even if the new opportunity has a stage in the databaseThis could hannen if the CRM lookun doesn't return stage informationRecommendation: Consider checking if the opportunity has a stage id and using that as a fallbackh ohoiSopportunity d Sthis->ooportunity 1d == Sooportunity->id)Sthis->opportunity id = Sopportunity→>id;nes to avoid stale stage from previous opportunity matchSthis-sstage id = Sstage?->id 22 Sonportunitv->stage id:However. lookina at the context. Sstage Is derived trom the CR lookup results, so ifit's null, the opportunity micht notnave stage intormation avallable. Une current denavior is acceptable.2. FixActivitiesOpportunity command (lines 123-128)"phoSactivity->updatedopportunity_id' => Sopportunity->getid(),Stade10 = sopportunzty-oerstadesdnNo issues found - This correctlv undates both fields toaethenOther locatione reviewed.•IntegrationApp/Service.php (line 560): Sets stage id directly from opportunity→>stage id when creating/updatingfrom lead data - this is correct• Copy command: Copies both opportunity id and stage_id together - this is correct.ActivitvController: Handles ctaae id from ucer reauects - not related to the bualTest Results• Uodatecrmbatatest : 2 tests passed (6 assertions)• MatchGrmObiectTect : 15 tocts nasced (75 accertions))The implementation is sound. The only minor concern is the null stage case in updateActivityCrmData(), but this isaccentable aiven the conteyt.Toa1l "• Your included weekly usage quota is exhausted. Purchase extra usage to continue using premium models. Quota resets May 17, 11:00Lets have÷ @Code SWF-1.6W Windsurf TeamoPo. 4 spa...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
42829
|
1569
|
1
|
2026-05-14T12:06:48.138719+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760408138_m1.jpg...
|
PhpStorm
|
faVsco.js – Activity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
DMSActivityFilesLaterMore+Slack>0.(ahlSupport D DMSActivityFilesLaterMore+Slack>0.(ahlSupport Daily - nowFileEditViewGoHistoryJiminny ...# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...•- Direct messages. Galya Dimit...C. Vasil VasilevP. Aneliya AngelovaStefka Stoyanova&: Todor StamatovMario GeorgievNikolay Ivanov2o James Graham. Stoyan TanevSteliyan GeorgievPetko KashinskiLukas Kovalik y...:: AppsToastJira CloudConala CalaWindowHelpDescribe what you are looking forVasil VasilevSupport Dailynow - 15:00-15:15= Notes - Support Daily.•MessagesAdd canvasO Files& Pins +ние имаме някаква история на сделката, имаме стейдж по време наToday ~на активититоно тоя стейдж го ползваме само за това тьрсене в on demand (edited)преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insightsимаме дата на отваряне и затваряне на сделказащо да не сложим и един маркер кога се е променил стейджатака в таймлайна ще се вижда "развитието" на тая сделкаLukas Kovalik 2:45 PMхм ами то дали да го няма вечеVasil Vasilev 2:45 PMняма гоLukas Kovalik 2:46 PMняма дано ВЕ си го врьщас тази цел точноVasil Vasilev 2:47 PMзнам, проверих, преди да предложа на Галя да ги покажемвЪЗМоЖНоно явно не е направеноVasil Vasilev 3:04 PMв крайна сметка тоя метод Activity:updateActivityCrmData се вика само при подмяна на opportunity, нали така?Lukas Kovalik 3:05 PMгледам го сегаMessage Vasil Vasilev+100% C8• Thu 14 May 15:06:48C Join Google Meet...
|
NULL
|
1562447289425726774
|
NULL
|
app_switch
|
ocr
|
NULL
|
DMSActivityFilesLaterMore+Slack>0.(ahlSupport D DMSActivityFilesLaterMore+Slack>0.(ahlSupport Daily - nowFileEditViewGoHistoryJiminny ...# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...•- Direct messages. Galya Dimit...C. Vasil VasilevP. Aneliya AngelovaStefka Stoyanova&: Todor StamatovMario GeorgievNikolay Ivanov2o James Graham. Stoyan TanevSteliyan GeorgievPetko KashinskiLukas Kovalik y...:: AppsToastJira CloudConala CalaWindowHelpDescribe what you are looking forVasil VasilevSupport Dailynow - 15:00-15:15= Notes - Support Daily.•MessagesAdd canvasO Files& Pins +ние имаме някаква история на сделката, имаме стейдж по време наToday ~на активититоно тоя стейдж го ползваме само за това тьрсене в on demand (edited)преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insightsимаме дата на отваряне и затваряне на сделказащо да не сложим и един маркер кога се е променил стейджатака в таймлайна ще се вижда "развитието" на тая сделкаLukas Kovalik 2:45 PMхм ами то дали да го няма вечеVasil Vasilev 2:45 PMняма гоLukas Kovalik 2:46 PMняма дано ВЕ си го врьщас тази цел точноVasil Vasilev 2:47 PMзнам, проверих, преди да предложа на Галя да ги покажемвЪЗМоЖНоно явно не е направеноVasil Vasilev 3:04 PMв крайна сметка тоя метод Activity:updateActivityCrmData се вика само при подмяна на opportunity, нали така?Lukas Kovalik 3:05 PMгледам го сегаMessage Vasil Vasilev+100% C8• Thu 14 May 15:06:48C Join Google Meet...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
42831
|
1569
|
2
|
2026-05-14T12:06:50.446364+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760410446_m1.jpg...
|
PhpStorm
|
faVsco.js – Activity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
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: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preference' => 'boolean',
'recording_reason_code' => 'integer',
'has_recording_prompt' => 'boolean',
'on_air' => 'integer',
];
}
protected static function boot()
{
parent::boot();
static::updated(static function (Activity $activity) {
// If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week
if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||
($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {
if ($activity->isDirty('status')) {
event(new StatusUpdated($activity));
}
if ($activity->isDirty('stage_id')) {
event(new StageUpdated($activity));
}
if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {
event(new ProspectUpdated($activity));
}
if ($activity->isDirty('opportunity_id')) {
event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));
}
if ($activity->isDirty('title')) {
event(new TitleUpdated($activity));
}
}
if ($activity->isDirty('playbook_category_id')) {
event(new ActivityTypeUpdated($activity));
}
});
static::deleted(static function (Activity $activity) {
// Hard delete associated playlistActivities
$activity->playlistActivities()->delete();
});
}
public function getOrganizerAttribute(): ?Participant
{
$participant = $this->participants()->where('user_id', $this->user_id)->first();
if (! $participant instanceof Participant && $participant !== null) {
throw new RuntimeException(sprintf('$participant must be an instance of "%s" or null', Participant::class));
}
return $participant;
}
public function getFormattedValueAttribute()
{
$currencyCode = 'USD';
if ($this->opportunity) {
$currencyCode = $this->opportunity->getCurrencyCode();
}
$formatter = new CurrencyFormatter();
$formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);
$formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);
return $formatter->format($this->value, $currencyCode);
}
public function getProspectNameAttribute(): ?string
{
$prospectName = null;
if ($this->lead_id) {
$prospectName = $this->lead->name;
} elseif ($this->contact_id) {
$prospectName = $this->contact->name;
} elseif ($this->account_id) {
$prospectName = $this->account->name;
}
return $prospectName;
}
public function getProspectName(): ?string
{
/** @var string|null */
return $this->getAttribute('prospect_name');
}
/**
* Get activity title depending on prospect or title
*/
public function getActivityTitleAttribute(): ?string
{
$activityTitle = null;
if ($this->prospect && $this->prospect->getName()) {
if ($this->account_id) {
$activityTitle = $this->account->name;
} elseif ($this->lead_id) {
$activityTitle = $this->lead->company;
} elseif ($this->contact_id) {
$activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;
}
} elseif ($this->title) {
$activityTitle = $this->title;
}
return $activityTitle;
}
public function wasRecentlyCreated(): bool
{
return $this->wasRecentlyCreated;
}
public function getProspectTypeAttribute()
{
$prospectType = null;
if ($this->lead_id) {
$prospectType = 'Lead';
} elseif ($this->contact_id) {
$prospectType = 'Contact';
} elseif ($this->account_id) {
$prospectType = 'Account';
}
return $prospectType;
}
/**
* Return the best match for prospect. Results are in the following order of priority:
* 1. Lead
* 2. Contact
* 3. Account
* 4. NULL
*/
public function getProspectAttribute(): ?ProspectInterface
{
if ($this->hasLead()) {
return $this->getLead();
}
if ($this->hasContact()) {
return $this->getContact();
}
if ($this->hasAccount()) {
return $this->getAccount();
}
return null;
}
public function getTitleAttribute($value): ?string
{
return \getActivityTitleAttribute(
$this->user->name,
$this->getType(),
$value,
$this->prospect->name ?? null,
$this->from->national_phone_number ?? null
);
}
public function getTitle(): ?string
{
return $this->getAttribute('title');
}
public function getSummary(): ?string
{
return $this->getAttribute('summary');
}
public funct...
|
[{"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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","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":"<?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 ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->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 ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->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":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}]...
|
6328793513270926623
|
6990582668119975740
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
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: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preference' => 'boolean',
'recording_reason_code' => 'integer',
'has_recording_prompt' => 'boolean',
'on_air' => 'integer',
];
}
protected static function boot()
{
parent::boot();
static::updated(static function (Activity $activity) {
// If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week
if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||
($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {
if ($activity->isDirty('status')) {
event(new StatusUpdated($activity));
}
if ($activity->isDirty('stage_id')) {
event(new StageUpdated($activity));
}
if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {
event(new ProspectUpdated($activity));
}
if ($activity->isDirty('opportunity_id')) {
event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));
}
if ($activity->isDirty('title')) {
event(new TitleUpdated($activity));
}
}
if ($activity->isDirty('playbook_category_id')) {
event(new ActivityTypeUpdated($activity));
}
});
static::deleted(static function (Activity $activity) {
// Hard delete associated playlistActivities
$activity->playlistActivities()->delete();
});
}
public function getOrganizerAttribute(): ?Participant
{
$participant = $this->participants()->where('user_id', $this->user_id)->first();
if (! $participant instanceof Participant && $participant !== null) {
throw new RuntimeException(sprintf('$participant must be an instance of "%s" or null', Participant::class));
}
return $participant;
}
public function getFormattedValueAttribute()
{
$currencyCode = 'USD';
if ($this->opportunity) {
$currencyCode = $this->opportunity->getCurrencyCode();
}
$formatter = new CurrencyFormatter();
$formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);
$formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);
return $formatter->format($this->value, $currencyCode);
}
public function getProspectNameAttribute(): ?string
{
$prospectName = null;
if ($this->lead_id) {
$prospectName = $this->lead->name;
} elseif ($this->contact_id) {
$prospectName = $this->contact->name;
} elseif ($this->account_id) {
$prospectName = $this->account->name;
}
return $prospectName;
}
public function getProspectName(): ?string
{
/** @var string|null */
return $this->getAttribute('prospect_name');
}
/**
* Get activity title depending on prospect or title
*/
public function getActivityTitleAttribute(): ?string
{
$activityTitle = null;
if ($this->prospect && $this->prospect->getName()) {
if ($this->account_id) {
$activityTitle = $this->account->name;
} elseif ($this->lead_id) {
$activityTitle = $this->lead->company;
} elseif ($this->contact_id) {
$activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;
}
} elseif ($this->title) {
$activityTitle = $this->title;
}
return $activityTitle;
}
public function wasRecentlyCreated(): bool
{
return $this->wasRecentlyCreated;
}
public function getProspectTypeAttribute()
{
$prospectType = null;
if ($this->lead_id) {
$prospectType = 'Lead';
} elseif ($this->contact_id) {
$prospectType = 'Contact';
} elseif ($this->account_id) {
$prospectType = 'Account';
}
return $prospectType;
}
/**
* Return the best match for prospect. Results are in the following order of priority:
* 1. Lead
* 2. Contact
* 3. Account
* 4. NULL
*/
public function getProspectAttribute(): ?ProspectInterface
{
if ($this->hasLead()) {
return $this->getLead();
}
if ($this->hasContact()) {
return $this->getContact();
}
if ($this->hasAccount()) {
return $this->getAccount();
}
return null;
}
public function getTitleAttribute($value): ?string
{
return \getActivityTitleAttribute(
$this->user->name,
$this->getType(),
$value,
$this->prospect->name ?? null,
$this->from->national_phone_number ?? null
);
}
public function getTitle(): ?string
{
return $this->getAttribute('title');
}
public function getSummary(): ?string
{
return $this->getAttribute('summary');
}
public funct...
|
42829
|
NULL
|
NULL
|
NULL
|
|
42832
|
1570
|
1
|
2026-05-14T12:06:54.023245+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760414023_m2.jpg...
|
PhpStorm
|
faVsco.js – Activity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
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: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preference' => 'boolean',
'recording_reason_code' => 'integer',
'has_recording_prompt' => 'boolean',
'on_air' => 'integer',
];
}
protected static function boot()
{
parent::boot();
static::updated(static function (Activity $activity) {
// If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week
if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||
($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {
if ($activity->isDirty('status')) {
event(new StatusUpdated($activity));
}
if ($activity->isDirty('stage_id')) {
event(new StageUpdated($activity));
}
if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {
event(new ProspectUpdated($activity));
}
if ($activity->isDirty('opportunity_id')) {
event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));
}
if ($activity->isDirty('title')) {
event(new TitleUpdated($activity));
}
}
if ($activity->isDirty('playbook_category_id')) {
event(new ActivityTypeUpdated($activity));
}
});
static::deleted(static function (Activity $activity) {
// Hard delete associated playlistActivities
$activity->playlistActivities()->delete();
});
}
public function getOrganizerAttribute(): ?Participant
{
$participant = $this->participants()->where('user_id', $this->user_id)->first();
if (! $participant instanceof Participant && $participant !== null) {
throw new RuntimeException(sprintf('$participant must be an instance of "%s" or null', Participant::class));
}
return $participant;
}
public function getFormattedValueAttribute()
{
$currencyCode = 'USD';
if ($this->opportunity) {
$currencyCode = $this->opportunity->getCurrencyCode();
}
$formatter = new CurrencyFormatter();
$formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);
$formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);
return $formatter->format($this->value, $currencyCode);
}
public function getProspectNameAttribute(): ?string
{
$prospectName = null;
if ($this->lead_id) {
$prospectName = $this->lead->name;
} elseif ($this->contact_id) {
$prospectName = $this->contact->name;
} elseif ($this->account_id) {
$prospectName = $this->account->name;
}
return $prospectName;
}
public function getProspectName(): ?string
{
/** @var string|null */
return $this->getAttribute('prospect_name');
}
/**
* Get activity title depending on prospect or title
*/
public function getActivityTitleAttribute(): ?string
{
$activityTitle = null;
if ($this->prospect && $this->prospect->getName()) {
if ($this->account_id) {
$activityTitle = $this->account->name;
} elseif ($this->lead_id) {
$activityTitle = $this->lead->company;
} elseif ($this->contact_id) {
$activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;
}
} elseif ($this->title) {
$activityTitle = $this->title;
}
return $activityTitle;
}
public function wasRecentlyCreated(): bool
{
return $this->wasRecentlyCreated;
}
public function getProspectTypeAttribute()
{
$prospectType = null;
if ($this->lead_id) {
$prospectType = 'Lead';
} elseif ($this->contact_id) {
$prospectType = 'Contact';
} elseif ($this->account_id) {
$prospectType = 'Account';
}
return $prospectType;
}
/**
* Return the best match for prospect. Results are in the following order of priority:
* 1. Lead
* 2. Contact
* 3. Account
* 4. NULL
*/
public function getProspectAttribute(): ?ProspectInterface
{
if ($this->hasLead()) {
return $this->getLead();
}
if ($this->hasContact()) {
return $this->getContact();
}
if ($this->hasAccount()) {
return $this->getAccount();
}
return null;
}
public function getTitleAttribute($value): ?string
{
return \getActivityTitleAttribute(
$this->user->name,
$this->getType(),
$value,
$this->prospect->name ?? null,
$this->from->national_phone_number ?? null
);
}
public function getTitle(): ?string
{
return $this->getAttribute('title');
}
public function getSummary(): ?string
{
return $this->getAttribute('summary');
}
public funct...
|
[{"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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"1","depth":4,"bounds":{"left":0.38464096,"top":0.22426178,"width":0.00731383,"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\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"bounds":{"left":0.122340426,"top":0.0,"width":0.2869016,"height":1.0},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"4","depth":4,"bounds":{"left":0.6665558,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"bounds":{"left":0.6765292,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.69049203,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"bounds":{"left":0.70046544,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"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.73138297,"top":0.123703115,"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 ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->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 ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->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}]...
|
6328793513270926623
|
6990582668119975740
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
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: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preference' => 'boolean',
'recording_reason_code' => 'integer',
'has_recording_prompt' => 'boolean',
'on_air' => 'integer',
];
}
protected static function boot()
{
parent::boot();
static::updated(static function (Activity $activity) {
// If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week
if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||
($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {
if ($activity->isDirty('status')) {
event(new StatusUpdated($activity));
}
if ($activity->isDirty('stage_id')) {
event(new StageUpdated($activity));
}
if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {
event(new ProspectUpdated($activity));
}
if ($activity->isDirty('opportunity_id')) {
event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));
}
if ($activity->isDirty('title')) {
event(new TitleUpdated($activity));
}
}
if ($activity->isDirty('playbook_category_id')) {
event(new ActivityTypeUpdated($activity));
}
});
static::deleted(static function (Activity $activity) {
// Hard delete associated playlistActivities
$activity->playlistActivities()->delete();
});
}
public function getOrganizerAttribute(): ?Participant
{
$participant = $this->participants()->where('user_id', $this->user_id)->first();
if (! $participant instanceof Participant && $participant !== null) {
throw new RuntimeException(sprintf('$participant must be an instance of "%s" or null', Participant::class));
}
return $participant;
}
public function getFormattedValueAttribute()
{
$currencyCode = 'USD';
if ($this->opportunity) {
$currencyCode = $this->opportunity->getCurrencyCode();
}
$formatter = new CurrencyFormatter();
$formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);
$formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);
return $formatter->format($this->value, $currencyCode);
}
public function getProspectNameAttribute(): ?string
{
$prospectName = null;
if ($this->lead_id) {
$prospectName = $this->lead->name;
} elseif ($this->contact_id) {
$prospectName = $this->contact->name;
} elseif ($this->account_id) {
$prospectName = $this->account->name;
}
return $prospectName;
}
public function getProspectName(): ?string
{
/** @var string|null */
return $this->getAttribute('prospect_name');
}
/**
* Get activity title depending on prospect or title
*/
public function getActivityTitleAttribute(): ?string
{
$activityTitle = null;
if ($this->prospect && $this->prospect->getName()) {
if ($this->account_id) {
$activityTitle = $this->account->name;
} elseif ($this->lead_id) {
$activityTitle = $this->lead->company;
} elseif ($this->contact_id) {
$activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;
}
} elseif ($this->title) {
$activityTitle = $this->title;
}
return $activityTitle;
}
public function wasRecentlyCreated(): bool
{
return $this->wasRecentlyCreated;
}
public function getProspectTypeAttribute()
{
$prospectType = null;
if ($this->lead_id) {
$prospectType = 'Lead';
} elseif ($this->contact_id) {
$prospectType = 'Contact';
} elseif ($this->account_id) {
$prospectType = 'Account';
}
return $prospectType;
}
/**
* Return the best match for prospect. Results are in the following order of priority:
* 1. Lead
* 2. Contact
* 3. Account
* 4. NULL
*/
public function getProspectAttribute(): ?ProspectInterface
{
if ($this->hasLead()) {
return $this->getLead();
}
if ($this->hasContact()) {
return $this->getContact();
}
if ($this->hasAccount()) {
return $this->getAccount();
}
return null;
}
public function getTitleAttribute($value): ?string
{
return \getActivityTitleAttribute(
$this->user->name,
$this->getType(),
$value,
$this->prospect->name ?? null,
$this->from->national_phone_number ?? null
);
}
public function getTitle(): ?string
{
return $this->getAttribute('title');
}
public function getSummary(): ?string
{
return $this->getAttribute('summary');
}
public funct...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
42833
|
1570
|
2
|
2026-05-14T12:06:58.989635+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760418989_m2.jpg...
|
PhpStorm
|
faVsco.js – Activity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}...
|
[{"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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"1","depth":4,"bounds":{"left":0.38464096,"top":0.22426178,"width":0.00731383,"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\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"bounds":{"left":0.122340426,"top":0.0,"width":0.2869016,"height":1.0},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-3756722355735473141
|
-2001763810168969368
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}...
|
42832
|
NULL
|
NULL
|
NULL
|
|
42836
|
1570
|
3
|
2026-05-14T12:06:59.805423+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760419805_m2.jpg...
|
PhpStorm
|
faVsco.js – Activity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'...
|
[{"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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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}]...
|
6501133838611896727
|
-7484559978919525948
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
PhostormVIewINavicarecodeFV faVsco.js( #12077 on JY-20903-update_activity-stage-on....Proiect v© TextRelayService.php© SyncPlanhat.php© Sso.php(©) ValidateSendingMessage.pnpO syncuserrllot.onp© ValidateSendingMessage.p© ActivityRepository.phpValladtesenaingnotricatio© EventserviceProvider.pnpM WehhookE.gitkeep© ChangeLogContextCorrelatiov @ Mailv @ Activities© ExportLink.php© MailBoxFailedToConnect.p© ProspectCache.php© StaleRecordValidator.phpC) OpportunityRepository.php© UpdateSinaleEntity.phpC) MatchActivityCrmData.php(c) Service.php© Client.phewOpportunitySyncTrait.php(C) PayloadBuilder.php(c) Confiquration.phpclass ImportActivityuypes impLements ShouldQueueImporc che scandara evenc lask lype pickuisc opcions from the cki) smskeceivea.php© SmsRelavFailed.phppubuic tunccion handleur aybookureared sevenc: voldc) Trackkestored.ondC7 CalendarsSolavbook = Sevent->plavbook:>D crm0 Reports(c) Mailable.oho// Don't run if somehow we already have categories.1f Solavbook->qetcategonies@->1sNotEmotv00 <Jiminny Models Acuivity.upoateActivilycrmbata in Prolect rilesv Methodm) d undateActivitvermData Activitv lann/Modelsv Usages in Project Files 20 resultsMethod call 4 recultsv Ca app 4 resultsv D app/Listeners/Activities/Crm 2 results© Crm/UpdateCrmData.php 1 resultv (C) UindateCrmData 1 recultlv mi d handla 1 rocult->updateAcuivilycrmbata srecoras"> © MatchCrmObject.php 1 result> O app/Services/Calendar/Command 1 resuh> C app/Services/Crm 1 result> Unclassified 16 resultssupoont Dally • now100% L2• Thu 14 May 15:06:59U AskJiminnyReportActivityServiceTest v= custom.logscratch &.ison= laravel.logA SF jiminny@localhost]& HS_local [jiminny@localhost]console [PRODIA console (EU]tiò accounts [EU]fii stages (EU]tid teams [EU]© Activity.php XPipedrive SDK EvaluaFixing Activity Stage DImport CRM Activity T+0 ..© FixActivitiesOpportunity.php© Opportunity.php& console SlAGiNGI've Implemented Fix 4 in two locations:1. Activ1ty::updateAct1v1tycrmbata) Unes 2122-2126class Accvicy excenas model Imolemencs84 B169 M4 M 102 24 ^contactnulupho2088Stage|null,strinalnul*} Srecords1t (Sopportunity Sthis→>opportunity 1d .== Sopportunity->1d) <Pines-ropporcuntlystu = sopportunztyzoeu,changes to avoid stale stage from previous opportunity matchmA1л v- 2091=2092sthis->stage 1d = Sstage7->1d:public function updateActivityCrmData(array Srecords): void1.407209%// Extract the records.[Slead, Saccount,$opportunity, Scontact, $stage] = Srecords;Potential Issue: If Sstage is null, this will set stage id to null even if the new opportunity has a stage in the databaseThis could hannen if the CRM lookun doesn't return stage information209520962097Sresolver = $this->getUpdateCrmDataResolverO:Sstrategy = Sresolver->resolveForActivity(Slead, $contact, $account):• XouGinTadeed weekly usage quota is exnausted. Purchase extta usage to continue using premium models. Quota resets May 1/, 11:00Lets naveif (Sstrategy == UpdateCrmDataByStrategy::Lead) {I/ Also update the parent activity if required, checking we don't create a mixed lead/account record.+ <> Code SWE-1.6$prospectSearchStrategy = ProspectSearchStrategyFactory::match(Sthis->team):if (SprospectSearchStrategy->ignoreCrmMatchDataO) {// Unset any associated crm DataSthis->logger->info('[UpdateCrmData] Ignoring crm data because of prospect strategy'. [acclvity-1d → saculvity-gecto'strategy' => get_class($prospectSearchStrategy).} else {Srecords = Sthis->updateParticipantsCrmData(ScrmService, Sactivity, Sparticipants):if (! empty(Srecords)) {Sactivity->updateActivityCrmData(Srecords)Now we know who the meetina is with. try and re-assian ownershio if necessarv.dispatch(new_Assian0wnershio(Sactivitv->aetUuid@)):* @param Collection<Participant> Sparticipants* Qreturn arraudLeadlnull.Accountlnul1lOnnortunitulnulllContac+ nu17Staao nul7stringlnull*}/array-i}Call HierarchyWN Windsurf Teams2001-22io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
42834
|
1569
|
3
|
2026-05-14T12:06:59.853127+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760419853_m1.jpg...
|
PhpStorm
|
faVsco.js – Activity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest...
|
[{"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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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}]...
|
4284113599095607475
|
-7484559978919001664
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
DMSActivityFilesLaterMore+Slack>0.(ahlSupport Daily - nowFileEditViewGoHistoryJiminny ...# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...•- Direct messages. Galya Dimit...C. Vasil VasilevP. Aneliya AngelovaStefka Stoyanova&: Todor StamatovMario GeorgievNikolay Ivanov2o James Graham. Stoyan TanevSteliyan GeorgievPetko KashinskiLukas Kovalik y...:: AppsToastJira CloudConala CalaWindowHelpDescribe what you are looking forVasil VasilevSupport Dailynow - 15:00-15:15= Notes - Support Daily.•MessagesAdd canvasO Files& Pins +ние имаме някаква история на сделката, имаме стейдж по време наToday ~на активититоно тоя стейдж го ползваме само за това тьрсене в on demand (edited)преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insightsимаме дата на отваряне и затваряне на сделказащо да не сложим и един маркер кога се е променил стейджатака в таймлайна ще се вижда "развитието" на тая сделкаLukas Kovalik 2:45 PMхм ами то дали да го няма вечеVasil Vasilev 2:45 PMняма гоLukas Kovalik 2:46 PMняма дано ВЕ си го врьщас тази цел точноVasil Vasilev 2:47 PMзнам, проверих, преди да предложа на Галя да ги покажемвЪЗМоЖНоно явно не е направеноVasil Vasilev 3:04 PMв крайна сметка тоя метод Activity:updateActivityCrmData се вика само при подмяна на opportunity, нали така?Lukas Kovalik 3:05 PMгледам го сегаMessage Vasil Vasilev+100% C8• Thu 14 May 15:06:59C Join Google Meet...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
42835
|
1569
|
4
|
2026-05-14T12:07:01.471513+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760421471_m1.jpg...
|
PhpStorm
|
faVsco.js – Activity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
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: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preference' => 'boolean',
'recording_reason_code' => 'integer',
'has_recording_prompt' => 'boolean',
'on_air' => 'integer',
];
}
protected static function boot()
{
parent::boot();
static::updated(static function (Activity $activity) {
// If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week
if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||
($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {
if ($activity->isDirty('status')) {
event(new StatusUpdated($activity));
}
if ($activity->isDirty('stage_id')) {
event(new StageUpdated($activity));
}
if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {
event(new ProspectUpdated($activity));
}
if ($activity->isDirty('opportunity_id')) {
event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));
}
if ($activity->isDirty('title')) {
event(new TitleUpdated($activity));
}
}
if ($activity->isDirty('playbook_category_id')) {
event(new ActivityTypeUpdated($activity));
}
});
static::deleted(static function (Activity $activity) {
// Hard delete associated playlistActivities
$activity->playlistActivities()->delete();
});
}
public function getOrganizerAttribute(): ?Participant
{
$participant = $this->participants()->where('user_id', $this->user_id)->first();
if (! $participant instanceof Participant && $participant !== null) {
throw new RuntimeException(sprintf('$participant must be an instance of "%s" or null', Participant::class));
}
return $participant;
}
public function getFormattedValueAttribute()
{
$currencyCode = 'USD';
if ($this->opportunity) {
$currencyCode = $this->opportunity->getCurrencyCode();
}
$formatter = new CurrencyFormatter();
$formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);
$formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);
return $formatter->format($this->value, $currencyCode);
}
public function getProspectNameAttribute(): ?string
{
$prospectName = null;
if ($this->lead_id) {
$prospectName = $this->lead->name;
} elseif ($this->contact_id) {
$prospectName = $this->contact->name;
} elseif ($this->account_id) {
$prospectName = $this->account->name;
}
return $prospectName;
}
public function getProspectName(): ?string
{
/** @var string|null */
return $this->getAttribute('prospect_name');
}
/**
* Get activity title depending on prospect or title
*/
public function getActivityTitleAttribute(): ?string
{
$activityTitle = null;
if ($this->prospect && $this->prospect->getName()) {
if ($this->account_id) {
$activityTitle = $this->account->name;
} elseif ($this->lead_id) {
$activityTitle = $this->lead->company;
} elseif ($this->contact_id) {
$activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;
}
} elseif ($this->title) {
$activityTitle = $this->title;
}
return $activityTitle;
}
public function wasRecentlyCreated(): bool
{
return $this->wasRecentlyCreated;
}
public function getProspectTypeAttribute()
{
$prospectType = null;
if ($this->lead_id) {
$prospectType = 'Lead';
} elseif ($this->contact_id) {
$prospectType = 'Contact';
} elseif ($this->account_id) {
$prospectType = 'Account';
}
return $prospectType;
}
/**
* Return the best match for prospect. Results are in the following order of priority:
* 1. Lead
* 2. Contact
* 3. Account
* 4. NULL
*/
public function getProspectAttribute(): ?ProspectInterface
{
if ($this->hasLead()) {
return $this->getLead();
}
if ($this->hasContact()) {
return $this->getContact();
}
if ($this->hasAccount()) {
return $this->getAccount();
}
return null;
}
public function getTitleAttribute($value): ?string
{
return \getActivityTitleAttribute(
$this->user->name,
$this->getType(),
$value,
$this->prospect->name ?? null,
$this->from->national_phone_number ?? null
);
}
public function getTitle(): ?string
{
return $this->getAttribute('title');
}
public function getSummary(): ?string
{
return $this->getAttribute('summary');
}
public funct...
|
[{"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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","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":"<?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 ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->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 ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->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,"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}]...
|
6328793513270926623
|
6990582668119975740
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
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: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preference' => 'boolean',
'recording_reason_code' => 'integer',
'has_recording_prompt' => 'boolean',
'on_air' => 'integer',
];
}
protected static function boot()
{
parent::boot();
static::updated(static function (Activity $activity) {
// If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week
if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||
($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {
if ($activity->isDirty('status')) {
event(new StatusUpdated($activity));
}
if ($activity->isDirty('stage_id')) {
event(new StageUpdated($activity));
}
if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {
event(new ProspectUpdated($activity));
}
if ($activity->isDirty('opportunity_id')) {
event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));
}
if ($activity->isDirty('title')) {
event(new TitleUpdated($activity));
}
}
if ($activity->isDirty('playbook_category_id')) {
event(new ActivityTypeUpdated($activity));
}
});
static::deleted(static function (Activity $activity) {
// Hard delete associated playlistActivities
$activity->playlistActivities()->delete();
});
}
public function getOrganizerAttribute(): ?Participant
{
$participant = $this->participants()->where('user_id', $this->user_id)->first();
if (! $participant instanceof Participant && $participant !== null) {
throw new RuntimeException(sprintf('$participant must be an instance of "%s" or null', Participant::class));
}
return $participant;
}
public function getFormattedValueAttribute()
{
$currencyCode = 'USD';
if ($this->opportunity) {
$currencyCode = $this->opportunity->getCurrencyCode();
}
$formatter = new CurrencyFormatter();
$formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);
$formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);
return $formatter->format($this->value, $currencyCode);
}
public function getProspectNameAttribute(): ?string
{
$prospectName = null;
if ($this->lead_id) {
$prospectName = $this->lead->name;
} elseif ($this->contact_id) {
$prospectName = $this->contact->name;
} elseif ($this->account_id) {
$prospectName = $this->account->name;
}
return $prospectName;
}
public function getProspectName(): ?string
{
/** @var string|null */
return $this->getAttribute('prospect_name');
}
/**
* Get activity title depending on prospect or title
*/
public function getActivityTitleAttribute(): ?string
{
$activityTitle = null;
if ($this->prospect && $this->prospect->getName()) {
if ($this->account_id) {
$activityTitle = $this->account->name;
} elseif ($this->lead_id) {
$activityTitle = $this->lead->company;
} elseif ($this->contact_id) {
$activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;
}
} elseif ($this->title) {
$activityTitle = $this->title;
}
return $activityTitle;
}
public function wasRecentlyCreated(): bool
{
return $this->wasRecentlyCreated;
}
public function getProspectTypeAttribute()
{
$prospectType = null;
if ($this->lead_id) {
$prospectType = 'Lead';
} elseif ($this->contact_id) {
$prospectType = 'Contact';
} elseif ($this->account_id) {
$prospectType = 'Account';
}
return $prospectType;
}
/**
* Return the best match for prospect. Results are in the following order of priority:
* 1. Lead
* 2. Contact
* 3. Account
* 4. NULL
*/
public function getProspectAttribute(): ?ProspectInterface
{
if ($this->hasLead()) {
return $this->getLead();
}
if ($this->hasContact()) {
return $this->getContact();
}
if ($this->hasAccount()) {
return $this->getAccount();
}
return null;
}
public function getTitleAttribute($value): ?string
{
return \getActivityTitleAttribute(
$this->user->name,
$this->getType(),
$value,
$this->prospect->name ?? null,
$this->from->national_phone_number ?? null
);
}
public function getTitle(): ?string
{
return $this->getAttribute('title');
}
public function getSummary(): ?string
{
return $this->getAttribute('summary');
}
public funct...
|
42834
|
NULL
|
NULL
|
NULL
|
|
42838
|
1569
|
5
|
2026-05-14T12:07:06.633378+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760426633_m1.jpg...
|
PhpStorm
|
faVsco.js – Activity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-544778921353285778
|
-7015200798817571900
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
HomeDMSActivityFilesLaterMore+Slack>0.(ahlSupport Daily - nowFileEditViewGoHistoryJiminny ...# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...•- Direct messages. Galya Dimit...C. Vasil VasilevP. Aneliya AngelovaStefka Stoyanova&: Todor StamatovMario GeorgievNikolay Ivanov2o James Graham. Stoyan TanevSteliyan GeorgievPetko KashinskiLukas Kovalik y...:: AppsToastJira CloudConala CalaWindowHelpDescribe what you are looking forVasil VasilevSupport Dailynow - 15:00-15:15= Notes - Support Daily.•MessagesAdd canvasO Files& Pins +ние имаме някаква история на сделката, имаме стейдж по време наToday ~на активититоно тоя стейдж го ползваме само за това тьрсене в on demand (edited)преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insightsимаме дата на отваряне и затваряне на сделказащо да не сложим и един маркер кога се е променил стейджатака в таймлайна ще се вижда "развитието" на тая сделкаLukas Kovalik 2:45 PMхм ами то дали да го няма вечеVasil Vasilev 2:45 PMняма гоLukas Kovalik 2:46 PMняма дано ВЕ си го врьщас тази цел точноVasil Vasilev 2:47 PMзнам, проверих, преди да предложа на Галя да ги покажемвЪЗМоЖНоно явно не е направеноVasil Vasilev 3:04 PMв крайна сметка тоя метод Activity:updateActivityCrmData се вика само при подмяна на opportunity, нали така?Lukas Kovalik 3:05 PMгледам го сегаMessage Vasil Vasilev+100% C8• Thu 14 May 15:07:06C Join Google Meet...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
42837
|
1570
|
4
|
2026-05-14T12:07:06.662363+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760426662_m2.jpg...
|
PhpStorm
|
faVsco.js – Activity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
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: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preference' => 'boolean',
'recording_reason_code' => 'integer',
'has_recording_prompt' => 'boolean',
'on_air' => 'integer',
];
}
protected static function boot()
{
parent::boot();
static::updated(static function (Activity $activity) {
// If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week
if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||
($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {
if ($activity->isDirty('status')) {
event(new StatusUpdated($activity));
}
if ($activity->isDirty('stage_id')) {
event(new StageUpdated($activity));
}
if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {
event(new ProspectUpdated($activity));
}
if ($activity->isDirty('opportunity_id')) {
event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));
}
if ($activity->isDirty('title')) {
event(new TitleUpdated($activity));
}
}
if ($activity->isDirty('playbook_category_id')) {
event(new ActivityTypeUpdated($activity));
}
});
static::deleted(static function (Activity $activity) {
// Hard delete associated playlistActivities
$activity->playlistActivities()->delete();
});
}
public function getOrganizerAttribute(): ?Participant
{
$participant = $this->participants()->where('user_id', $this->user_id)->first();
if (! $participant instanceof Participant && $participant !== null) {
throw new RuntimeException(sprintf('$participant must be an instance of "%s" or null', Participant::class));
}
return $participant;
}
public function getFormattedValueAttribute()
{
$currencyCode = 'USD';
if ($this->opportunity) {
$currencyCode = $this->opportunity->getCurrencyCode();
}
$formatter = new CurrencyFormatter();
$formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);
$formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);
return $formatter->format($this->value, $currencyCode);
}
public function getProspectNameAttribute(): ?string
{
$prospectName = null;
if ($this->lead_id) {
$prospectName = $this->lead->name;
} elseif ($this->contact_id) {
$prospectName = $this->contact->name;
} elseif ($this->account_id) {
$prospectName = $this->account->name;
}
return $prospectName;
}
public function getProspectName(): ?string
{
/** @var string|null */
return $this->getAttribute('prospect_name');
}
/**
* Get activity title depending on prospect or title
*/
public function getActivityTitleAttribute(): ?string
{
$activityTitle = null;
if ($this->prospect && $this->prospect->getName()) {
if ($this->account_id) {
$activityTitle = $this->account->name;
} elseif ($this->lead_id) {
$activityTitle = $this->lead->company;
} elseif ($this->contact_id) {
$activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;
}
} elseif ($this->title) {
$activityTitle = $this->title;
}
return $activityTitle;
}
public function wasRecentlyCreated(): bool
{
return $this->wasRecentlyCreated;
}
public function getProspectTypeAttribute()
{
$prospectType = null;
if ($this->lead_id) {
$prospectType = 'Lead';
} elseif ($this->contact_id) {
$prospectType = 'Contact';
} elseif ($this->account_id) {
$prospectType = 'Account';
}
return $prospectType;
}
/**
* Return the best match for prospect. Results are in the following order of priority:
* 1. Lead
* 2. Contact
* 3. Account
* 4. NULL
*/
public function getProspectAttribute(): ?ProspectInterface
{
if ($this->hasLead()) {
return $this->getLead();
}
if ($this->hasContact()) {
return $this->getContact();
}
if ($this->hasAccount()) {
return $this->getAccount();
}
return null;
}
public function getTitleAttribute($value): ?string
{
return \getActivityTitleAttribute(
$this->user->name,
$this->getType(),
$value,
$this->prospect->name ?? null,
$this->from->national_phone_number ?? null
);
}
public function getTitle(): ?string
{
return $this->getAttribute('title');
}
public function getSummary(): ?string
{
return $this->getAttribute('summary');
}
public funct...
|
[{"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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"1","depth":4,"bounds":{"left":0.38464096,"top":0.22426178,"width":0.00731383,"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\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"bounds":{"left":0.122340426,"top":0.0,"width":0.2869016,"height":1.0},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"4","depth":4,"bounds":{"left":0.6665558,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"bounds":{"left":0.6765292,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.69049203,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"bounds":{"left":0.70046544,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"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.73138297,"top":0.123703115,"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 ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->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 ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->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}]...
|
6328793513270926623
|
6990582668119975740
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
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: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preference' => 'boolean',
'recording_reason_code' => 'integer',
'has_recording_prompt' => 'boolean',
'on_air' => 'integer',
];
}
protected static function boot()
{
parent::boot();
static::updated(static function (Activity $activity) {
// If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week
if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||
($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {
if ($activity->isDirty('status')) {
event(new StatusUpdated($activity));
}
if ($activity->isDirty('stage_id')) {
event(new StageUpdated($activity));
}
if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {
event(new ProspectUpdated($activity));
}
if ($activity->isDirty('opportunity_id')) {
event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));
}
if ($activity->isDirty('title')) {
event(new TitleUpdated($activity));
}
}
if ($activity->isDirty('playbook_category_id')) {
event(new ActivityTypeUpdated($activity));
}
});
static::deleted(static function (Activity $activity) {
// Hard delete associated playlistActivities
$activity->playlistActivities()->delete();
});
}
public function getOrganizerAttribute(): ?Participant
{
$participant = $this->participants()->where('user_id', $this->user_id)->first();
if (! $participant instanceof Participant && $participant !== null) {
throw new RuntimeException(sprintf('$participant must be an instance of "%s" or null', Participant::class));
}
return $participant;
}
public function getFormattedValueAttribute()
{
$currencyCode = 'USD';
if ($this->opportunity) {
$currencyCode = $this->opportunity->getCurrencyCode();
}
$formatter = new CurrencyFormatter();
$formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);
$formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);
return $formatter->format($this->value, $currencyCode);
}
public function getProspectNameAttribute(): ?string
{
$prospectName = null;
if ($this->lead_id) {
$prospectName = $this->lead->name;
} elseif ($this->contact_id) {
$prospectName = $this->contact->name;
} elseif ($this->account_id) {
$prospectName = $this->account->name;
}
return $prospectName;
}
public function getProspectName(): ?string
{
/** @var string|null */
return $this->getAttribute('prospect_name');
}
/**
* Get activity title depending on prospect or title
*/
public function getActivityTitleAttribute(): ?string
{
$activityTitle = null;
if ($this->prospect && $this->prospect->getName()) {
if ($this->account_id) {
$activityTitle = $this->account->name;
} elseif ($this->lead_id) {
$activityTitle = $this->lead->company;
} elseif ($this->contact_id) {
$activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;
}
} elseif ($this->title) {
$activityTitle = $this->title;
}
return $activityTitle;
}
public function wasRecentlyCreated(): bool
{
return $this->wasRecentlyCreated;
}
public function getProspectTypeAttribute()
{
$prospectType = null;
if ($this->lead_id) {
$prospectType = 'Lead';
} elseif ($this->contact_id) {
$prospectType = 'Contact';
} elseif ($this->account_id) {
$prospectType = 'Account';
}
return $prospectType;
}
/**
* Return the best match for prospect. Results are in the following order of priority:
* 1. Lead
* 2. Contact
* 3. Account
* 4. NULL
*/
public function getProspectAttribute(): ?ProspectInterface
{
if ($this->hasLead()) {
return $this->getLead();
}
if ($this->hasContact()) {
return $this->getContact();
}
if ($this->hasAccount()) {
return $this->getAccount();
}
return null;
}
public function getTitleAttribute($value): ?string
{
return \getActivityTitleAttribute(
$this->user->name,
$this->getType(),
$value,
$this->prospect->name ?? null,
$this->from->national_phone_number ?? null
);
}
public function getTitle(): ?string
{
return $this->getAttribute('title');
}
public function getSummary(): ?string
{
return $this->getAttribute('summary');
}
public funct...
|
42836
|
NULL
|
NULL
|
NULL
|
|
42839
|
1569
|
6
|
2026-05-14T12:07:08.103333+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760428103_m1.jpg...
|
PhpStorm
|
faVsco.js – Activity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
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: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preference' => 'boolean',
'recording_reason_code' => 'integer',
'has_recording_prompt' => 'boolean',
'on_air' => 'integer',
];
}
protected static function boot()
{
parent::boot();
static::updated(static function (Activity $activity) {
// If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week
if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||
($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {
if ($activity->isDirty('status')) {
event(new StatusUpdated($activity));
}
if ($activity->isDirty('stage_id')) {
event(new StageUpdated($activity));
}
if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {
event(new ProspectUpdated($activity));
}
if ($activity->isDirty('opportunity_id')) {
event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));
}
if ($activity->isDirty('title')) {
event(new TitleUpdated($activity));
}
}
if ($activity->isDirty('playbook_category_id')) {
event(new ActivityTypeUpdated($activity));
}
});
static::deleted(static function (Activity $activity) {
// Hard delete associated playlistActivities
$activity->playlistActivities()->delete();
});
}
public function getOrganizerAttribute(): ?Participant
{
$participant = $this->participants()->where('user_id', $this->user_id)->first();
if (! $participant instanceof Participant && $participant !== null) {
throw new RuntimeException(sprintf('$participant must be an instance of "%s" or null', Participant::class));
}
return $participant;
}
public function getFormattedValueAttribute()
{
$currencyCode = 'USD';
if ($this->opportunity) {
$currencyCode = $this->opportunity->getCurrencyCode();
}
$formatter = new CurrencyFormatter();
$formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);
$formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);
return $formatter->format($this->value, $currencyCode);
}
public function getProspectNameAttribute(): ?string
{
$prospectName = null;
if ($this->lead_id) {
$prospectName = $this->lead->name;
} elseif ($this->contact_id) {
$prospectName = $this->contact->name;
} elseif ($this->account_id) {
$prospectName = $this->account->name;
}
return $prospectName;
}
public function getProspectName(): ?string
{
/** @var string|null */
return $this->getAttribute('prospect_name');
}
/**
* Get activity title depending on prospect or title
*/
public function getActivityTitleAttribute(): ?string
{
$activityTitle = null;
if ($this->prospect && $this->prospect->getName()) {
if ($this->account_id) {
$activityTitle = $this->account->name;
} elseif ($this->lead_id) {
$activityTitle = $this->lead->company;
} elseif ($this->contact_id) {
$activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;
}
} elseif ($this->title) {
$activityTitle = $this->title;
}
return $activityTitle;
}
public function wasRecentlyCreated(): bool
{
return $this->wasRecentlyCreated;
}
public function getProspectTypeAttribute()
{
$prospectType = null;
if ($this->lead_id) {
$prospectType = 'Lead';
} elseif ($this->contact_id) {
$prospectType = 'Contact';
} elseif ($this->account_id) {
$prospectType = 'Account';
}
return $prospectType;
}
/**
* Return the best match for prospect. Results are in the following order of priority:
* 1. Lead
* 2. Contact
* 3. Account
* 4. NULL
*/
public function getProspectAttribute(): ?ProspectInterface
{
if ($this->hasLead()) {
return $this->getLead();
}
if ($this->hasContact()) {
return $this->getContact();
}
if ($this->hasAccount()) {
return $this->getAccount();
}
return null;
}
public function getTitleAttribute($value): ?string
{
return \getActivityTitleAttribute(
$this->user->name,
$this->getType(),
$value,
$this->prospect->name ?? null,
$this->from->national_phone_number ?? null
);
}
public function getTitle(): ?string
{
return $this->getAttribute('title');
}
public function getSummary(): ?string
{
return $this->getAttribute('summary');
}
public funct...
|
[{"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":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","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":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Crm;\n\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Events\\Playbooks\\PlaybookCreated;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldValue;\nuse Jiminny\\Models\\PlaybookCategory;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\PlaybookCategoryRepository;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse stdClass;\nuse Throwable;\n\nclass ImportActivityTypes implements ShouldQueue\n{\n /**\n * Create the event listener.\n */\n public function __construct(\n private readonly ResolveTeamCrmConnection $crmResolver,\n private readonly FieldRepository $fieldRepository,\n private readonly PlaybookCategoryRepository $repository,\n ) {\n // nothing\n }\n\n /**\n * Import the standard Event/Task Type picklist options from the CRM.\n */\n public function handle(PlaybookCreated $event): void\n {\n $playbook = $event->playbook;\n\n // Don't run if somehow we already have categories.\n if ($playbook->getCategories()->isNotEmpty()) {\n return;\n }\n\n $values = [];\n\n try {\n $crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());\n $crmService->syncField($playbook->getActivityField());\n $values = $crmService->importPicklistValues($playbook->getActivityField());\n } catch (Throwable $e) {\n Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'error' => $e->getMessage(),\n ]);\n }\n\n if (empty($values)) {\n $values = $this->fetchActivityFieldValues($playbook->getActivityField());\n\n Log::info('[ImportActivityTypes] Using database fallback for categories', [\n 'playbook_id' => $playbook->getId(),\n 'field_values_count' => $values->count(),\n ]);\n }\n\n $createdCount = 0;\n\n /** @var stdClass{label: string} $value */\n foreach ($values as $value) {\n $data = [\n 'name' => $value->label,\n 'enabled' => true,\n 'type' => PlaybookCategory::TYPE_ALL,\n ];\n\n if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;\n }\n\n if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {\n $data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;\n }\n\n $this->repository->create($playbook, $data);\n $createdCount++;\n }\n\n if ($createdCount === 0) {\n Log::warning('[ImportActivityTypes] No categories created for playbook', [\n 'playbook_id' => $playbook->getId(),\n 'team_id' => $playbook->getTeamId(),\n 'field_id' => $playbook->getActivityField()?->getId(),\n ]);\n }\n }\n\n private function fetchActivityFieldValues(Field $field): Collection\n {\n /** @var Collection<FieldValue> */\n return $this->fieldRepository->getPicklistValues($field);\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":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","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":"<?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 ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->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 ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->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,"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}]...
|
6328793513270926623
|
6990582668119975740
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, 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
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Crm;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Jiminny\Events\Playbooks\PlaybookCreated;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldValue;
use Jiminny\Models\PlaybookCategory;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\PlaybookCategoryRepository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\ResolveTeamCrmConnection;
use stdClass;
use Throwable;
class ImportActivityTypes implements ShouldQueue
{
/**
* Create the event listener.
*/
public function __construct(
private readonly ResolveTeamCrmConnection $crmResolver,
private readonly FieldRepository $fieldRepository,
private readonly PlaybookCategoryRepository $repository,
) {
// nothing
}
/**
* Import the standard Event/Task Type picklist options from the CRM.
*/
public function handle(PlaybookCreated $event): void
{
$playbook = $event->playbook;
// Don't run if somehow we already have categories.
if ($playbook->getCategories()->isNotEmpty()) {
return;
}
$values = [];
try {
$crmService = $this->crmResolver->resolveForTeam($playbook->getTeam());
$crmService->syncField($playbook->getActivityField());
$values = $crmService->importPicklistValues($playbook->getActivityField());
} catch (Throwable $e) {
Log::warning('[ImportActivityTypes] CRM API failed, falling back to database values', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'error' => $e->getMessage(),
]);
}
if (empty($values)) {
$values = $this->fetchActivityFieldValues($playbook->getActivityField());
Log::info('[ImportActivityTypes] Using database fallback for categories', [
'playbook_id' => $playbook->getId(),
'field_values_count' => $values->count(),
]);
}
$createdCount = 0;
/** @var stdClass{label: string} $value */
foreach ($values as $value) {
$data = [
'name' => $value->label,
'enabled' => true,
'type' => PlaybookCategory::TYPE_ALL,
];
if (Str::contains(strtolower($value->label), ['sms sent', 'sms out', 'text in'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_OUTBOUND;
}
if (Str::contains(strtolower($value->label), ['sms received', 'sms in', 'text out'])) {
$data['type'] = PlaybookCategory::TYPE_SMS_INBOUND;
}
$this->repository->create($playbook, $data);
$createdCount++;
}
if ($createdCount === 0) {
Log::warning('[ImportActivityTypes] No categories created for playbook', [
'playbook_id' => $playbook->getId(),
'team_id' => $playbook->getTeamId(),
'field_id' => $playbook->getActivityField()?->getId(),
]);
}
}
private function fetchActivityFieldValues(Field $field): Collection
{
/** @var Collection<FieldValue> */
return $this->fieldRepository->getPicklistValues($field);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
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: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preference' => 'boolean',
'recording_reason_code' => 'integer',
'has_recording_prompt' => 'boolean',
'on_air' => 'integer',
];
}
protected static function boot()
{
parent::boot();
static::updated(static function (Activity $activity) {
// If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week
if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||
($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {
if ($activity->isDirty('status')) {
event(new StatusUpdated($activity));
}
if ($activity->isDirty('stage_id')) {
event(new StageUpdated($activity));
}
if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {
event(new ProspectUpdated($activity));
}
if ($activity->isDirty('opportunity_id')) {
event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));
}
if ($activity->isDirty('title')) {
event(new TitleUpdated($activity));
}
}
if ($activity->isDirty('playbook_category_id')) {
event(new ActivityTypeUpdated($activity));
}
});
static::deleted(static function (Activity $activity) {
// Hard delete associated playlistActivities
$activity->playlistActivities()->delete();
});
}
public function getOrganizerAttribute(): ?Participant
{
$participant = $this->participants()->where('user_id', $this->user_id)->first();
if (! $participant instanceof Participant && $participant !== null) {
throw new RuntimeException(sprintf('$participant must be an instance of "%s" or null', Participant::class));
}
return $participant;
}
public function getFormattedValueAttribute()
{
$currencyCode = 'USD';
if ($this->opportunity) {
$currencyCode = $this->opportunity->getCurrencyCode();
}
$formatter = new CurrencyFormatter();
$formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);
$formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);
return $formatter->format($this->value, $currencyCode);
}
public function getProspectNameAttribute(): ?string
{
$prospectName = null;
if ($this->lead_id) {
$prospectName = $this->lead->name;
} elseif ($this->contact_id) {
$prospectName = $this->contact->name;
} elseif ($this->account_id) {
$prospectName = $this->account->name;
}
return $prospectName;
}
public function getProspectName(): ?string
{
/** @var string|null */
return $this->getAttribute('prospect_name');
}
/**
* Get activity title depending on prospect or title
*/
public function getActivityTitleAttribute(): ?string
{
$activityTitle = null;
if ($this->prospect && $this->prospect->getName()) {
if ($this->account_id) {
$activityTitle = $this->account->name;
} elseif ($this->lead_id) {
$activityTitle = $this->lead->company;
} elseif ($this->contact_id) {
$activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;
}
} elseif ($this->title) {
$activityTitle = $this->title;
}
return $activityTitle;
}
public function wasRecentlyCreated(): bool
{
return $this->wasRecentlyCreated;
}
public function getProspectTypeAttribute()
{
$prospectType = null;
if ($this->lead_id) {
$prospectType = 'Lead';
} elseif ($this->contact_id) {
$prospectType = 'Contact';
} elseif ($this->account_id) {
$prospectType = 'Account';
}
return $prospectType;
}
/**
* Return the best match for prospect. Results are in the following order of priority:
* 1. Lead
* 2. Contact
* 3. Account
* 4. NULL
*/
public function getProspectAttribute(): ?ProspectInterface
{
if ($this->hasLead()) {
return $this->getLead();
}
if ($this->hasContact()) {
return $this->getContact();
}
if ($this->hasAccount()) {
return $this->getAccount();
}
return null;
}
public function getTitleAttribute($value): ?string
{
return \getActivityTitleAttribute(
$this->user->name,
$this->getType(),
$value,
$this->prospect->name ?? null,
$this->from->national_phone_number ?? null
);
}
public function getTitle(): ?string
{
return $this->getAttribute('title');
}
public function getSummary(): ?string
{
return $this->getAttribute('summary');
}
public funct...
|
42838
|
NULL
|
NULL
|
NULL
|
|
42841
|
1570
|
5
|
2026-05-14T12:07:20.241101+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760440241_m2.jpg...
|
Slack
|
Vasil Vasilev (DM) - Jiminny Inc - 4 new items - S Vasil Vasilev (DM) - Jiminny Inc - 4 new items - Slack...
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files...
|
[{"role":"AXPopUpButton","text [{"role":"AXPopUpButton","text":"Switch workspaces… (Jiminny Inc) Has new messages","depth":14,"bounds":{"left":0.27593085,"top":1.0,"width":0.011968086,"height":-0.058260202},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Home","depth":14,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Home","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"DMs","depth":14,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"DMs","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Activity","depth":14,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Activity","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":14,"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4955353713447221713
|
-8120951495743817545
|
click
|
hybrid
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
PhostormVIewcodeFV faVsco.js( #12077 on JY-20903-update_activity-stageProiect v© TextRelayService.php© SyncPlanhat.php© Sso.php© ValidateSendingMessage.phpO syncuserrllot.onp© ActivityRepository.php© ValidateSendingMessage.pValladtesendingnotricallo© ConterencecrmmatcherJob.pnp© EventserviceProvider.pnpM WehhookE.gitkeep© ProspectCache.php© StaleRecordValidator.php© ChangeLogContextCorrelatiolC) OpportunityRepository.php© UpdateSingleEntity.phpC) MatchActivityCrmData.php(c) Service.php© Client.phev @ Mailv @ ActivitieswOpportunitySyncTrait.php(C) PayloadBuilder.php(c) Confiquration.phpclass ImportActivityuypes impLements ShouldQueuec) exportLink.php© MailBoxFailedToConnect.pImporc che scandara evenclask lype pickuisc opcions from che ckr) smskeceivea.php© SmsRelavFailed.phppublic function nandle(Playbookcreated sevent): voldc)Trackkestored.onoCalendarsSolavbook = Sevent->plavbook:>D crm0 Reports// Don't run if somehow we already have categories.(c) Mailable.oho1f Solavbook->qetcategonies@->1sNotEmotv00 <JiminnyModels Acuivity.upoateActivilycrmbata in Prolect rilesv Methodm) d undateActivitvermData Activitv lann/ModelsUsages in Project Files 20 resultsMethod call 4 recultsv Ca app 4 resultsv D app/Listeners/Activities/Crm 2 results© Crm/UpdateCrmData.php 1 result• (C) UndateCrmData 1 recultiv mid handla 1 rocult14 sactivity->updateAcuviyermbata srecoras› © MatchCrmObject.php 1 re:v @ app/Services/Calendar/Command 1 resultv © ImportParticipants.php 1 resultv (C) ImportParticipants 1 resultv M d updateActivityCrmData 1 result121 sthis->activitv->updateActivitvermData(Srecords):v M app/Services/Crm 1 resultv C crmActivtvservice.ono i resui• C) CrmActivtvService 1 result• (m) & uodateCrmData 1 resulti89 Sactivitv->uodateActivitvCrmData(Srecords):Unclassitied 16 results= custom.logscratch &.ison= laravel.logA SF jiminny@localhost]& ho_local Uiminny@localnost& console [PROD]A console (EU]tiò accounts [EU]ii stages (EU]iiò teams [EU]ImporbotkecoraingJoo.org© Activity.php X© FixActivitiesOpportunity.php© Opportunity.php& console SlAGiNGclass Accvicy excenas model Imolemencs84 B169 M4 M 102 24 ^2088mA1л v2091= 20927 20002095209620Y/contactnuluStagelnull,strinalnul*} Srecordspublic function updateActivityCrmData(array Srecords): void// Extract the records.[Slead, Saccount, $opportunity, $contact, $stage] = Srecords;Ssesotven e Stess-sen-ypesolveontafevstverlead, Scontact, Saccount):if (Sstrategy == UpdateCrmDataByStrategy::Lead) ‹I/ Also update the parent activity if required, checking we don't create a mixed lead/account record.helsupoont Dally • now100% S2. Thu 14 May 15:07:19AskJiminnyReportActivityServiceTest vCascadePipedrive SDK EvaluaFixing Activity Stage DImport CRM Activity T+0 ..I've Implemented Fix 4 in two locations:1. Activ1ty: :updateAct1v1tyermData) (unes 2122-2126.pho1t (Sopportunity Sthis→>opportunity 1d .== Sopportunity->1d) <Pines-ropporcuntlystu = sopportunztyzoeu,changes to avoid stale stage from previous opportunity matchsthis->stage 1d = Sstage7->1d:Potential Issue: If Sstage is null, this will set stage id to null even if the new opportunity has a stage in the databaseThis could hannen if the CRM lookun doesn't return stage information• XouGiTAdoawoowvnesdloaliarslerawhsttehodwmensdoowrlcsaloaicaniatnltonctnaitntLets nave+ <> Code SWE-1.6Select item to previevCall HierarchyW Windsurf Teams 2091:32 UTF-8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|