|
54516
|
1896
|
47
|
2026-05-18T13:13:52.329642+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110032329_m1.jpg...
|
PhpStorm
|
faVsco.js – Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp[Platform) Refinemen… 47 m leftmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.comNikolay Yankov (Presenting)100% C28• Mon 18 May 16:13:526M InbeTAMINSTYS(18® For you© Recent|# Starred8? Apps0 SpacesSuarco(9 Service-DeskJiminny (Oid)Jiminny (New)1 CD Platform TeamCID Capture Team|IID Enterprise Stabllity L.(ID Processing TeamCID SE Kanban= More spaces= FiltersCB Dashboards€ OperationsE3 Confluence38 Team-% Customise sidebarCO Atte* Ciau• Cos: @ a0p:C Alor Pipe | © imi smi JmiTa Y-a tr-T tY-QIx FProhttps://fiminny.atlassian.net/browse/JY-20879C Projects• Home | Salestorce•0 8 dtd O @ cud Occae serty XiC DevQ Search |+ CreateSpaces / Jiminey (Nem) /, Aod parereWJY-2057• tne message snoua stay onry whse they are adoing tne activity type - & tney seave of retresn tne page it snoua asappear, use the into message tnat we naveLinked work itemsAdd linked work itemActivityCommentsHistoryWork logAdd a comment...Suggest a reply…Who is working on this..?siatas upossePro tip: press M to comesenNikolay YankovQuestions:• We have CRM activity type sync for all CRMs except Zoho, should we do it for the rest, not only SF and Hubspot?|• When do we sync - on every change, like type changes for instance? When is the trigger?Estimate:• BE: Maybe 1 day? (Check questions first)• FE: 0.5 day• QA: 1 day |~ DetailsLabelsStory point estimateNoneStory PointsOrganisationsNonePriorityFix versionsNoneSeelrePlatform Sprint 5 Q2Days2.5Need QAYesCanny LinksOpen Canny LinksMore fields• smlAsk RovoNikolay YankovNikolay IvanovAneliya AngelovaGalya Dimitrova4:13 PM | [Platform] Refinement ®..•Lukas Kovalik1:00:19...
|
NULL
|
-358531440389984190
|
NULL
|
click
|
ocr
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp[Platform) Refinemen… 47 m leftmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.comNikolay Yankov (Presenting)100% C28• Mon 18 May 16:13:526M InbeTAMINSTYS(18® For you© Recent|# Starred8? Apps0 SpacesSuarco(9 Service-DeskJiminny (Oid)Jiminny (New)1 CD Platform TeamCID Capture Team|IID Enterprise Stabllity L.(ID Processing TeamCID SE Kanban= More spaces= FiltersCB Dashboards€ OperationsE3 Confluence38 Team-% Customise sidebarCO Atte* Ciau• Cos: @ a0p:C Alor Pipe | © imi smi JmiTa Y-a tr-T tY-QIx FProhttps://fiminny.atlassian.net/browse/JY-20879C Projects• Home | Salestorce•0 8 dtd O @ cud Occae serty XiC DevQ Search |+ CreateSpaces / Jiminey (Nem) /, Aod parereWJY-2057• tne message snoua stay onry whse they are adoing tne activity type - & tney seave of retresn tne page it snoua asappear, use the into message tnat we naveLinked work itemsAdd linked work itemActivityCommentsHistoryWork logAdd a comment...Suggest a reply…Who is working on this..?siatas upossePro tip: press M to comesenNikolay YankovQuestions:• We have CRM activity type sync for all CRMs except Zoho, should we do it for the rest, not only SF and Hubspot?|• When do we sync - on every change, like type changes for instance? When is the trigger?Estimate:• BE: Maybe 1 day? (Check questions first)• FE: 0.5 day• QA: 1 day |~ DetailsLabelsStory point estimateNoneStory PointsOrganisationsNonePriorityFix versionsNoneSeelrePlatform Sprint 5 Q2Days2.5Need QAYesCanny LinksOpen Canny LinksMore fields• smlAsk RovoNikolay YankovNikolay IvanovAneliya AngelovaGalya Dimitrova4:13 PM | [Platform] Refinement ®..•Lukas Kovalik1:00:19...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
54517
|
1897
|
25
|
2026-05-18T13:13:52.336287+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110032336_m2.jpg...
|
PhpStorm
|
faVsco.js – Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormVIewINavicarecodeLaravelKeractorFV faVsco. PhpStormVIewINavicarecodeLaravelKeractorFV faVsco.js?9 master kProiectC BaseService.php© SoftPhoneManager.php(C) CoreUserRequest.onpc ScimProvisioning.ong© CoreUser.phpstageredosilony.ontsuoschipuonsetkepository.ong© TaskRepository.php© TeamAiContextRepository.php© TextMessagingService.phpleambomainskepository.ong© TeamInsightsRepository.phpsyncrieldsec WIclass Service extends ActivityProviderService implements ImportsDataInteBSAS0 MIN3 MLAVleamkeposilorv.ono© ThemeRepository.php136 Cpublic function importData(?Carbon $from = null, ?Carbon $to = null, ?User $user = null, ?string $aC) Timezonerepositorv.phpc) Topicrepositorv.php138c) TopictriaaerRepositorv.php139C) TrackRepositorv.ohp140c) TranscriptionModelLocaleRepositorv.oho141c)TranscriotionReositorv.ono142© TranscriptionSummaryRepository-phpC) UserRenositorv.oho11431144© VocabularyRepository.php1451• D RulesCommaSeparatedRoles.php146© CrmReference.php© DependentRolesRule.phpC) DomainValidator nhn© ListenerRoleCannotHaveAdminOrManagerPert 151C) 1 istenerRoleRequiresListenerFeatureRule.phpC) MultidimensionalArravMaxcharRule.ono0 NolnvitationsForEmailRule.php© NoTeamMemberWithEmailRule.phpce DarentNode.onp© RolesAndCrmOptionMismatchRule.php(c) SmsMessage.phpc) TransterUserAdminRule.phpv @ Servicesv D Activity> 0 Aircall> M AmazonConneciDAdOlO> MAvava> BaseService> M Bloobirds161162163164165166v m CloseC) Client.ohoC) Service.nhni> DCloudCallTM CloudTallTM ConnectAndSellN DemoDeck.M Dialnad1N SiahtRvFichtSivoNingMemai> 0 Gonqif (SactivityId) {$request = 'activity/call/' . SactivityId . '/':Sresponse = Sthis->client->request('GET'. Srequest)return (ant) sthis->importcall(response)sdateriltered = Sfrom I Sto:if ($from === null) {Svnc records that have been chanaed since we last imoorted. otherwise default to lookiiSfrom = Sthis->orovider->last svnced at 22 now@->subHour@1f (cto === null)Cto = nowd•SsvncedAt = nowd•$syncCount = 0;$lastCallId = null;Spayload = ['date_created lt' => $to->toIso8601String(.if (Suser) {Spayloadl'user 1d = Suser->ema1lsdofspayloadl"date_created__gt = $from->tols086015tring0Sreauest ='activity/caul?'htto build auervSoavload)Sresponse = Sthis->cuient->request('GET', Sreauest):SprocessedcallCount= 0.foreach ($response['data'] as $call) {Scallid = Scallfiadili* CSlastCallld === ScallTd)&I= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING]© CoachingFeedbackCoachUserln.php Xstohedeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 Ф >34 đ >45 đ >118119130135136 @t›146 Gt151 C)1561usadeorivate const int No GROUP 10 = 999-3 usagessyncFieldsService.phporivate UserRepository s!v Inherited members (&R)Anonymous Classes (96l)Lambdas (SL)nublic function constrpublic function shouldAppublic function getQueripublic function toArray(private function getOptipublic function getValueprivate function getDefapublic function aetValidpublic function aetSortopublic function shouldBe# [Platform] Refinemen... 47 m left100% 1• Mon 18 May 16:13:52AskJiminnyReportActivityServiceTest vCascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSCIM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for methodw9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ ATTR_ ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabled•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/Ueer/M PoleAttr.nhn +17-ites/User/ ẞ RoleAttrTest.nhn +224* Reiect alliiAccent alliAsk anvthina (&4-L)« Code SWF-1.6WN Windsurf Teams151-10UTF.8io 4 spaces...
|
NULL
|
812425539797533825
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormVIewINavicarecodeLaravelKeractorFV faVsco. PhpStormVIewINavicarecodeLaravelKeractorFV faVsco.js?9 master kProiectC BaseService.php© SoftPhoneManager.php(C) CoreUserRequest.onpc ScimProvisioning.ong© CoreUser.phpstageredosilony.ontsuoschipuonsetkepository.ong© TaskRepository.php© TeamAiContextRepository.php© TextMessagingService.phpleambomainskepository.ong© TeamInsightsRepository.phpsyncrieldsec WIclass Service extends ActivityProviderService implements ImportsDataInteBSAS0 MIN3 MLAVleamkeposilorv.ono© ThemeRepository.php136 Cpublic function importData(?Carbon $from = null, ?Carbon $to = null, ?User $user = null, ?string $aC) Timezonerepositorv.phpc) Topicrepositorv.php138c) TopictriaaerRepositorv.php139C) TrackRepositorv.ohp140c) TranscriptionModelLocaleRepositorv.oho141c)TranscriotionReositorv.ono142© TranscriptionSummaryRepository-phpC) UserRenositorv.oho11431144© VocabularyRepository.php1451• D RulesCommaSeparatedRoles.php146© CrmReference.php© DependentRolesRule.phpC) DomainValidator nhn© ListenerRoleCannotHaveAdminOrManagerPert 151C) 1 istenerRoleRequiresListenerFeatureRule.phpC) MultidimensionalArravMaxcharRule.ono0 NolnvitationsForEmailRule.php© NoTeamMemberWithEmailRule.phpce DarentNode.onp© RolesAndCrmOptionMismatchRule.php(c) SmsMessage.phpc) TransterUserAdminRule.phpv @ Servicesv D Activity> 0 Aircall> M AmazonConneciDAdOlO> MAvava> BaseService> M Bloobirds161162163164165166v m CloseC) Client.ohoC) Service.nhni> DCloudCallTM CloudTallTM ConnectAndSellN DemoDeck.M Dialnad1N SiahtRvFichtSivoNingMemai> 0 Gonqif (SactivityId) {$request = 'activity/call/' . SactivityId . '/':Sresponse = Sthis->client->request('GET'. Srequest)return (ant) sthis->importcall(response)sdateriltered = Sfrom I Sto:if ($from === null) {Svnc records that have been chanaed since we last imoorted. otherwise default to lookiiSfrom = Sthis->orovider->last svnced at 22 now@->subHour@1f (cto === null)Cto = nowd•SsvncedAt = nowd•$syncCount = 0;$lastCallId = null;Spayload = ['date_created lt' => $to->toIso8601String(.if (Suser) {Spayloadl'user 1d = Suser->ema1lsdofspayloadl"date_created__gt = $from->tols086015tring0Sreauest ='activity/caul?'htto build auervSoavload)Sresponse = Sthis->cuient->request('GET', Sreauest):SprocessedcallCount= 0.foreach ($response['data'] as $call) {Scallid = Scallfiadili* CSlastCallld === ScallTd)&I= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING]© CoachingFeedbackCoachUserln.php Xstohedeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 Ф >34 đ >45 đ >118119130135136 @t›146 Gt151 C)1561usadeorivate const int No GROUP 10 = 999-3 usagessyncFieldsService.phporivate UserRepository s!v Inherited members (&R)Anonymous Classes (96l)Lambdas (SL)nublic function constrpublic function shouldAppublic function getQueripublic function toArray(private function getOptipublic function getValueprivate function getDefapublic function aetValidpublic function aetSortopublic function shouldBe# [Platform] Refinemen... 47 m left100% 1• Mon 18 May 16:13:52AskJiminnyReportActivityServiceTest vCascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSCIM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for methodw9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ ATTR_ ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabled•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/Ueer/M PoleAttr.nhn +17-ites/User/ ẞ RoleAttrTest.nhn +224* Reiect alliiAccent alliAsk anvthina (&4-L)« Code SWF-1.6WN Windsurf Teams151-10UTF.8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
54518
|
1896
|
48
|
2026-05-18T13:13:55.075525+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110035075_m1.jpg...
|
PhpStorm
|
faVsco.js – Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp* [Platform) Refinemen... 47 m leftmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com100% C28• Mon 18 May 16:13:54Nikolay Yankov (Presenting)Edit•S*@ For you© Recent|# Starred8? Apps0 Spacesairco59 Service-DeskJiminny (Oid)Jiminny (New)1 CD Platform TeamCID Capture Team|CID Enterprise Stabllty L.CID Processing TeamCID SE Kanban= More spaces=Filtersua basncoards® Operationst: Assets$ Customise sidebarC AIDeC Sch© Cors* ciau• Com 8 a00. C AlovO Poe Jr m Jm4G0Z UY-•.xP Prolhttps:/fiminny.atlassian.net/browse/JY-20879- JmimC Projects|E3 SSH• HomelSalestrce B O 8 El Datadog 0 V Cuuse O CrcieciSerny *xL tase4 insights & Coachin.0 DewQ Search |Spaces Jminty (Nea)Addparert JY-20879Enable users to use their new activity typesDescriptionCurrently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomations that we have - this means they need to copy all of their prompts again. Which takes a huge amount of time.For SF and Hubspot• when a new activity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to all the playbooks where activity type field isChenuFor SF, Hubspot, Copper, Bullhorn and Pipedrive:• it a customer manuaty creates an activity type in Jiminny - show them a warning message on the top saying "Tf you want this activity type to syng to your CRM oneach call - make sure to have it in your CRM with the exact same name" - use thishttps://www.figma.com/design//XcUety9mx5Fiz8KosLAUn/Project-Phoenix?node-id-7483-2600188t-UJOZVDozYIpCSPAy-1• the message should stay only while they are adding the activity type - Il they leave or refresh the page it should disappear. Use the info message that we haveSubtasksAdd subtaskLinked work itemsAdd linked work itemActivitySuggest a reply....Who is working on this...?Status update...Pro tip: press M to comeserBackiogDetailsAssignee8 UnussignedAssign to meReporter2 Galya DimitrovaComponentsPtatformSub-ProductAdd optionsLabolsNoneStory point estimateNoneStory PointsNoneOrganisationsPriority= Medium|PIX VESONMon 18 May 16:13© JmmlAsk Rovo[3 Improve StoryNikolay YankovNikolay IvanovAneliya Angelovaelya Dimitrova4:13 PM | [Platform] Refinement ®...Lukas Kovalik1:00:22Lộ3...
|
NULL
|
2688730721586916009
|
NULL
|
visual_change
|
ocr
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp* [Platform) Refinemen... 47 m leftmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com100% C28• Mon 18 May 16:13:54Nikolay Yankov (Presenting)Edit•S*@ For you© Recent|# Starred8? Apps0 Spacesairco59 Service-DeskJiminny (Oid)Jiminny (New)1 CD Platform TeamCID Capture Team|CID Enterprise Stabllty L.CID Processing TeamCID SE Kanban= More spaces=Filtersua basncoards® Operationst: Assets$ Customise sidebarC AIDeC Sch© Cors* ciau• Com 8 a00. C AlovO Poe Jr m Jm4G0Z UY-•.xP Prolhttps:/fiminny.atlassian.net/browse/JY-20879- JmimC Projects|E3 SSH• HomelSalestrce B O 8 El Datadog 0 V Cuuse O CrcieciSerny *xL tase4 insights & Coachin.0 DewQ Search |Spaces Jminty (Nea)Addparert JY-20879Enable users to use their new activity typesDescriptionCurrently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomations that we have - this means they need to copy all of their prompts again. Which takes a huge amount of time.For SF and Hubspot• when a new activity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to all the playbooks where activity type field isChenuFor SF, Hubspot, Copper, Bullhorn and Pipedrive:• it a customer manuaty creates an activity type in Jiminny - show them a warning message on the top saying "Tf you want this activity type to syng to your CRM oneach call - make sure to have it in your CRM with the exact same name" - use thishttps://www.figma.com/design//XcUety9mx5Fiz8KosLAUn/Project-Phoenix?node-id-7483-2600188t-UJOZVDozYIpCSPAy-1• the message should stay only while they are adding the activity type - Il they leave or refresh the page it should disappear. Use the info message that we haveSubtasksAdd subtaskLinked work itemsAdd linked work itemActivitySuggest a reply....Who is working on this...?Status update...Pro tip: press M to comeserBackiogDetailsAssignee8 UnussignedAssign to meReporter2 Galya DimitrovaComponentsPtatformSub-ProductAdd optionsLabolsNoneStory point estimateNoneStory PointsNoneOrganisationsPriority= Medium|PIX VESONMon 18 May 16:13© JmmlAsk Rovo[3 Improve StoryNikolay YankovNikolay IvanovAneliya Angelovaelya Dimitrova4:13 PM | [Platform] Refinement ®...Lukas Kovalik1:00:22Lộ3...
|
54516
|
NULL
|
NULL
|
NULL
|
|
54519
|
1897
|
26
|
2026-05-18T13:13:56.609746+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110036609_m2.jpg...
|
PhpStorm
|
faVsco.js – Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
5222591262487654398
|
-8636637129462003327
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
PhostormVIewINavicareCodeLaravelKeractorFV faVsco.js v°9 master kProletey+.® ActivityController.php© CoreUserRequest.php© ThemeRepository.php© TimezoneRepository.php© TopicRepository.php© ТорicTriggerRepository.php© TrackRepository.php© TranscriptionModelLocaleRepository.php© TranscriptionRepository.php© TranscriptionSummaryRepository.php© UserRepository.php© VocabularyRepository.php~ D RulesCocommaseparateakoles.ong© CrmReference.php© DependentRolesRule.php© DomainValidator.php© ListenerRoleCannotHaveAdminOrManagerPermi:ListenerRoleRequiresListenerFeatureRule.phpscimProvistoning.ong© SoftPhoneManager.php© CoreUser.php© Service.php x © TextMessagingService.phpsyncrieldsx 5 cc w .*saphpw 45 430 MIY3 M2AVnanespace Jumanny services accrvity close• use ...class service excenas AccIVicyProviderservice 1mplements Imporcsuacalncertace28 0prorecred scrina sname = ACUIVICV::PKUVLUCKCLUSEYprivate Webhook suruGenerator:c) MultidimensionalArraymaxcharkule.pho© NoInvitationsForEmailRule.phpNoTeamMemberWithEmailRule.phpParentNode.php310 >public function -_construct(?array Sconfig)l...39 0>public function getbisplayName): stringt...C) RolesAndCrmOptionMismatchRule.ohv440)(c) Smsmessage.ohopublic function getInstallationStrategy(): stringf...;C) TransterUserAdminRule.oho49 g )public function canBeInstalled: booli...v _ Services~ DActivity→AircalllM AmazonConnect54 ot>public function onActivityProviderIntegrated(): voidf...}87 € >public function onActivityProviderEnabled(): voidf..,m Apollo• Avayam BaseServiceD Bloobirdsv MCloco© Client.php© Service.phpD CloudCallM CioudTall.O ConnectAndSellM DemoNeckDialpadEightByEight> D FiveNineGmail> [ Gona.GoogleMeet• D GoToMeetindD Groove> D HubSootImport•M.liminnviOJustCall92 € >public function onActivityProviderDisabled(): voidf...}107 €t >public function handLeActivityProviderCallback(array $payLoad){…;120 đ >public function supportedActivityChannels(): arrayf….,* @throws SocialAccountTokenInvalidException*othrows_HttpBadrequestzxception*othrows HttpForb1ddenzxceptzon* ochrows HttpNotFoundExceptzon* othrows ouotazxceededexception* othrows ServiceUnavarLableExceptionoublic function importData(2Carbon $from = null. 2Carbon $to = null. 2Usen Suser = null. ?string &if (SactivityId) {'activity/call' Sactivitvid . !!.Sresponse = $this->client->request('GET', Srequest):=custom.logA console [STAGING]stoheE laravel.log4 SF [jiminny@localhost]© CoachingFeedbackCoachUserin.phpxA HS_Jocal [jiminny@localhost]A console [PROD]# console [euydeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 đ >34 đ >45 đ >135136 @>146 @ >151 C)- 155usadeorivate const int No GROUP 10 = 9993 usagesprivate UserRepository $userRepository;public function __construct(UserRepository $userRepository){.}public function shouldApplyQueries(): boolf...}public function getQueries(): FilterDefinitionQueryCollectionf...+public function toArray(): arrayf...,private function getOptions(): arrayf..,public function getValue(): arrayf..,private function getDefaultValue(): arrayf….,public function getValidationRules(?string $prefix = null): arrayf…..,public function getSortOrder(): intf...}public function shouldßeIncluded(Team $team): b00lf...}( [Platform] Refinemen... 47 m leftU AskJiminnyReportActivityServiceTest~100% 18• Mon 18 May 16:13:56CascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSCIM Role Manageme+0 ..php on LineWARN Metadata found in doc-comment for methorw9 / 10 tasks done :• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META_ROLES and USER_PATCH_ATTR_ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabled•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintegration.8 Hes with chingon rapp/Component/SClM/ Constants.php +3app/Component/SCIM/ ScimProvisioning.php +85-15nse/D CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/Ueer/M PoleAttr.nhn +17-ites/Userl ẞ RoleAttrTest.ohn +224Ask anvthina (&4-L)« Code SWF-1.6* Reiect alliiAccent alliWN Windsurf Toams 157-27 UTF.8f 4 spaces...
|
54517
|
NULL
|
NULL
|
NULL
|
|
54521
|
1896
|
49
|
2026-05-18T13:13:58.082753+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110038082_m1.jpg...
|
PhpStorm
|
faVsco.js – Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4235983745889776938
|
-8204421443435123770
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp• [Platform) Refinemen….. 47 m left)meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com8• Mon 18 May 16:13:576Nikolay Yankov (Presenting)Tdt9(18® For you© Recent|# Starred8? Apps0 Spacesacirco59 Service-DeskJiminny (Oid)Jiminny (New)I CD Platform TeamCID Capture Team|ID Enterprise Stabllty L..(ID Processing TeamCID SE Kanban= More spaces= PinersCB Dashboards€ OperationsE3 Confiuence% Customise sidebarMon 18 May 16:13C Atteo Cur* ciau• Cos: 8 a0p C Alor Pipe Jmi sm Jmi2 tY-7 tY-F Prol© 3mhttps://fiminny.atlassian.net/browse/JY-20879— Утостя• Home | Salesforce•08 Ind O © Cuud OCrclaSentryL tasкLa insights & Coachin0 Dew+ CreateAsk RovoSpaces Jminny (New)Add parentHJY-20879Enable users to use their new activity typesBackiogI Improve StoryDetails• DescriptionCurrently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomacons that we have - this means they need to copy au or their prompes egan. wnich takes a nuge amourk or treFor SF and Hubspot• when a new activity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to all the playbooks where activity type field ischenesuAssignee8 UnassignedAssign to meКUРОRTER2 Galya DimitrovaFor SF, Hubspot, Copper, Bullhorn and Pipedrive:• il a customer manually creates an activity type in Jiminny - show them a warning message on the top saying "If you want this activity type to syne to your CRM oneach call - make sure to have ik in your CRM with the exact same name" - use this f https://www.figma.com/design/XcUety9mxSFizBKosl.AUn/Project-Phoenixinodc-id#7483-2800188t+uJO2VbpzYIpC5PAy-1|CoFigme aocos• the message should stay only while they are adding the activity type - Il they leave or refresh the page it should disappear. Use the info message that we haveQuick start developmentLink this work item to your code byinctuding keys when creatingobranch, commit, or pull requestbelow. Learn moreDismissSubtasksAdd subtaskUwewoetierQ Open with VS CodeLinked work itemsAdd linked work itemCreate commilComponentsActivityAlCommentsHistoryWork loaAdd optionsAdd a comment..Suggest a reply…..who ts workind on this.status upoaseLabelsNoneStory point estimateNikolay YankovNikolay IvanovAneliya AngelovaGalya Dimitrovag4:13 PM | [Platform] Refinement ®..•Lukas Kovalik1:00:25...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
54520
|
1897
|
27
|
2026-05-18T13:13:59.585958+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110039585_m2.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
6
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Zoom in
Open in new window
Enter Full Screen
Nikolay Yankov
Nikolay Ivanov
Aneliya Angelova
Galya Dimitrova...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - [Platform] Refinement 🔍","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.016123671,"height":-0.051875472},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.27094415,"top":1.0,"width":0.004986702,"height":-0.051875472},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.27310506,"top":1.0,"width":0.010638298,"height":-0.086193085},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Nikolay Yankov (Presenting)","depth":12,"bounds":{"left":0.30634972,"top":1.0,"width":0.059507977,"height":-0.072625756},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Yankov (Presenting)","depth":13,"bounds":{"left":0.30634972,"top":1.0,"width":0.059507977,"height":-0.07342374},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.69481385,"top":1.0,"width":0.019614361,"height":-0.06424582},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":22,"bounds":{"left":0.7081117,"top":1.0,"width":0.0023271276,"height":-0.072625756},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Take notes with Gemini","depth":14,"bounds":{"left":0.71708775,"top":1.0,"width":0.011968086,"height":-0.06424582},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Take notes with Gemini","depth":17,"bounds":{"left":0.7184175,"top":1.0,"width":0.043550532,"height":-0.072625756},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Gemini","depth":17,"bounds":{"left":0.7330452,"top":1.0,"width":0.013464096,"height":-0.072625756},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":17,"bounds":{"left":0.73204786,"top":1.0,"width":0.011303191,"height":-0.065043926},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Zoom in","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open in new window","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Enter Full Screen","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Nikolay Yankov","depth":17,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":17,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":17,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Galya Dimitrova","depth":17,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
8926575084908368126
|
3891527188381749766
|
click
|
accessibility
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
6
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Zoom in
Open in new window
Enter Full Screen
Nikolay Yankov
Nikolay Ivanov
Aneliya Angelova
Galya Dimitrova...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
54522
|
1896
|
50
|
2026-05-18T13:14:00.455620+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110040455_m1.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - [Platform] Refinement 🔍","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
338622676483802012
|
7377162633053750999
|
click
|
hybrid
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
FirefoxFileEditViewHistoryBookmarksProfiles→CToolsWindowHelp‹$0meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com8 [Platform] Refinemen... 47 m left100%8• Mon 18 May 16:14:00Nikolay Yankov (Presenting)+Mon 18 May 16:130 AtteC Scuhttps://fiminny.atlassian.net/browse/JY-20879C Projects© Curt* ciau• Com 0 a00oPioeom• Jm• JmiLa tY-a [Y-Z UY-F Prol0 sm03 sSSearch= Itame I Satesforce008 l Eod 9C*CusecrocetEsacyX+ CreateAsk Rovo® For you© Recent|# Starred8? AppsQ Spacesaarea(7 Service-DeskJiminny (0%a)Jiminny (New)Nikolay YankovNikolay IvanovAneliya AngelovaGalya DimitrovaLukas Kovalik4:13 PM | [Platform] Refinement •1:00:27...
|
54521
|
NULL
|
NULL
|
NULL
|
|
54523
|
1896
|
51
|
2026-05-18T13:14:01.405917+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110041405_m1.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
6
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Unpin Nikolay Yankov's presentation from your main screen
You can't unmute someone else's presentation
More options for Nikolay Yankov
Zoom in
Open in new window
Enter Full Screen
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
Pin Nikolay Ivanov to your main screen
You can't unmute someone else
More options for Nikolay Ivanov
Nikolay Ivanov
Pin Aneliya Angelova to your main screen
You can't unmute someone else
More options for Aneliya Angelova
Aneliya Angelova
Pin Galya Dimitrova to your main screen
Mute Galya Dimitrova's microphone
More options for Galya Dimitrova
Galya Dimitrova
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
Others might see more of your background. Click to view your full video....
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - [Platform] Refinement 🔍","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.8411111,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.0,"top":0.8794444,"width":0.033680554,"height":0.03888889},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.0,"top":0.91833335,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.95666665,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Nikolay Yankov (Presenting)","depth":12,"bounds":{"left":0.07534722,"top":0.101111114,"width":0.124305554,"height":0.022222223},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Yankov (Presenting)","depth":13,"bounds":{"left":0.07534722,"top":0.10222222,"width":0.124305554,"height":0.020555556},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.88680553,"top":0.08944444,"width":0.04097222,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":22,"bounds":{"left":0.9145833,"top":0.101111114,"width":0.0048611113,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Take notes with Gemini","depth":14,"bounds":{"left":0.93333334,"top":0.08944444,"width":0.025,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Take notes with Gemini","depth":17,"bounds":{"left":0.9361111,"top":0.101111114,"width":0.06388891,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Gemini","depth":17,"bounds":{"left":0.96666664,"top":0.101111114,"width":0.028125,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":17,"bounds":{"left":0.96458334,"top":0.090555556,"width":0.023611112,"height":0.037777778},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Unpin Nikolay Yankov's presentation from your main screen","depth":13,"bounds":{"left":0.34618056,"top":0.5088889,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else's presentation","depth":13,"bounds":{"left":0.37395832,"top":0.50666666,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Yankov","depth":13,"bounds":{"left":0.4045139,"top":0.5088889,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Zoom in","depth":13,"bounds":{"left":0.63090277,"top":0.78333336,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open in new window","depth":13,"bounds":{"left":0.6642361,"top":0.78333336,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Enter Full Screen","depth":13,"bounds":{"left":0.69756943,"top":0.78333336,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Pin Nikolay Yankov to your main screen","depth":13,"bounds":{"left":0.7607639,"top":0.25111112,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Mute Nikolay Yankov's microphone","depth":13,"bounds":{"left":0.7885417,"top":0.2488889,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Yankov","depth":13,"bounds":{"left":0.8190972,"top":0.25111112,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Nikolay Yankov","depth":17,"bounds":{"left":0.753125,"top":0.36277777,"width":0.07673611,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Nikolay Ivanov to your main screen","depth":13,"bounds":{"left":0.8871528,"top":0.25111112,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else","depth":13,"bounds":{"left":0.9149306,"top":0.2488889,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Ivanov","depth":13,"bounds":{"left":0.9454861,"top":0.25111112,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":17,"bounds":{"left":0.87951386,"top":0.36277777,"width":0.07395833,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Aneliya Angelova to your main screen","depth":13,"bounds":{"left":0.7607639,"top":0.5088889,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else","depth":13,"bounds":{"left":0.7885417,"top":0.50666666,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Aneliya Angelova","depth":13,"bounds":{"left":0.8190972,"top":0.5088889,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Aneliya Angelova","depth":17,"bounds":{"left":0.753125,"top":0.6205556,"width":0.088541664,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Galya Dimitrova to your main screen","depth":13,"bounds":{"left":0.8871528,"top":0.5088889,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Mute Galya Dimitrova's microphone","depth":13,"bounds":{"left":0.9149306,"top":0.50666666,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Galya Dimitrova","depth":13,"bounds":{"left":0.9454861,"top":0.5088889,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Galya Dimitrova","depth":17,"bounds":{"left":0.87951386,"top":0.6205556,"width":0.08194444,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"You’re continuously framed","depth":13,"bounds":{"left":0.82256943,"top":0.7644445,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Backgrounds and effects","depth":13,"bounds":{"left":0.853125,"top":0.7644445,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Lukas Kovalik","depth":13,"bounds":{"left":0.8836806,"top":0.76666665,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Lukas Kovalik","depth":17,"bounds":{"left":0.753125,"top":0.87833333,"width":0.06875,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Others might see more of your background. Click to view your full video.","depth":14,"bounds":{"left":0.96631944,"top":0.875,"width":0.018055556,"height":0.028888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
3398253015705739902
|
-573732331018266350
|
visual_change
|
hybrid
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
6
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Unpin Nikolay Yankov's presentation from your main screen
You can't unmute someone else's presentation
More options for Nikolay Yankov
Zoom in
Open in new window
Enter Full Screen
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
Pin Nikolay Ivanov to your main screen
You can't unmute someone else
More options for Nikolay Ivanov
Nikolay Ivanov
Pin Aneliya Angelova to your main screen
You can't unmute someone else
More options for Aneliya Angelova
Aneliya Angelova
Pin Galya Dimitrova to your main screen
Mute Galya Dimitrova's microphone
More options for Galya Dimitrova
Galya Dimitrova
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
Others might see more of your background. Click to view your full video.
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp§ [Platform] Refinemen... 46 m left100% C8• Mon 18 May 16:14:01meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.comNikolay Yankov (Presenting)+Mon 18 Mãy 16:14Si8® For you© Recent|# Starred8? AppsD Spacesaarea(9 Service-DeskJiminny (Osd)Jiminny (New)I ŒD Plattorm Team(D Capture TeamD Enterprise Stabilty L.(ID Processing TeamCID SE Kanban= More spaces= HiitersCB Dashboards6) Operations21 Assets?: Teanvl5; Customise sidebar0 Atte© Ourt* Cial• COR 8 a00 C Alor O Pipe| O Jmi Jm© JmilZ tY-a [Y-Z UY-https://fiminny.atlassian.net/browse/JY-20879C ProjectsO3 sS• Mtsne I Safosfarce |0& 0@1 0CEsetcy XSpacesJmnay (Nre)CAod garontWJY-20379Enable users to use their new activity types• DescriptionCurrently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomations that we nave - this toearis they neod to copy ar or theur prompes agar. wrich takes à nage amount or trneFor SF and Hubspot• when a new activity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to all the playbooks where activity type field isChetsoFor SF, Hubspot, Copper, Bullhorn and Pipedrive:• it a customer manuaty creates an actvity type in Jiminny - show them a warning message on the top saying "1f you want this actiiky type to symo to your CRM onesch call - make sure to have i& in your CRM with the exact same name" - ute this https://www.figma.com/des.pn/XCUefy9mxSFiz8KosLAUn/Project-Phoenixinode-id[PHONE]t»UJO2VDprY]pCSPAy-1• the message should stay only while they are adding the activity type - il they leave or refresh the page it should disappear. Use the info message that we haveSubtasksAdd subtaskCnxeewolktchnAdd linked work iter• ActivitySuggest a repty-Whio is working on this...?Status update...toto.tressteourProlC DoBackiogDetailsAssignee& UnassignedAssign to meReporterg Galya DimitrovaComponentsPlatformSub-ProductAdd optionsLabelsNoneStory point estimateNoneStory PointsNoneOrganisationsPriority= Medium|Pu yewon0 smAsk RovoZE Improve StoryNikolay YankovNikolay IvanovAneliya AngelovaGalya DimitrovasBe4:14 PM | [Platform] Refinement ®Lukas Kovalik1:00:28...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
54524
|
1896
|
52
|
2026-05-18T13:14:04.458071+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110044458_m1.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
6
Take notes with Gemini...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - [Platform] Refinement 🔍","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.8411111,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.0,"top":0.8794444,"width":0.033680554,"height":0.03888889},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.0,"top":0.91833335,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.95666665,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Nikolay Yankov (Presenting)","depth":12,"bounds":{"left":0.07534722,"top":0.101111114,"width":0.124305554,"height":0.022222223},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Yankov (Presenting)","depth":13,"bounds":{"left":0.07534722,"top":0.10222222,"width":0.124305554,"height":0.020555556},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.88680553,"top":0.08944444,"width":0.04097222,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":22,"bounds":{"left":0.9145833,"top":0.101111114,"width":0.0048611113,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Take notes with Gemini","depth":14,"bounds":{"left":0.93333334,"top":0.08944444,"width":0.025,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4907745723008448585
|
4467700968160785030
|
visual_change
|
hybrid
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
6
Take notes with Gemini
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp§ [Platform] Refinemen... 46 m left100% C8• Mon 18 May 16:14:04meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com=Nikolay Yankov (Presenting)+Mon 18 May 1€:14(18@ For you• Recent|# Starred8? Apps0 Spacesaurco(9 Service-DeskJiminny (Oid)Jiminny (New)1 CD Platform TeamCID Capture Team |IID Enterprise Stabllity L.(ID Processing TeamCID SE Kanban= More spaces= Filtersua basnooardOperations83 Confluence3: Teame* Customise sidebarC Alteo Ours Clas• Cos: | a0p C Alor | Pipe | Jmi Jmi Jimia шY-7 UY-F Proi• smlhttps://fminny.atlassian.net/browse/JY-20879JeeyC Projects |• Home| Salestoce @ O 8 E ElDatadog $e Claude O CircieciSentry *xL TaskInsights & Coachin0 DevSearch+ CreateAsk RovoSpacesJminny (New)C. Add paremHJY-20679Enable users to use their new activity typesBackiog1 Improve StoryDetailsDescriptionCurrently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomacons that we have - this means they need to copy ar of theur prompes agan. wnich takes a nuge amourk or üreFor SF and Huospot• when a new activity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to all the playbooks where activity type field ischeneeuAssignee& UnassignedAssign to meКOPOTTEN& Galya DimitrovaFor SF, Hubspot, Copper, Bullhorn and Pipedrive:• if a customer manually creates an activity type in Jiminny - show them a warning message on the top saying "If you want this activity type to syne to your CRM oneach call - make sure to have i in your CRM with the exact same name" - use this F https://www.figma.com/design/[XcUety9mxSFiz8KosLAUn/Project-Phoenix?node-id[PHONE]t•UJO2VbpzY]pC5PAy-1| Co• Figma acco• the message should stay only while they are adding the activity type - Il they leave or refresh the page it should disappear. Use the info message that we haveQuick start developmentLink this work item to your code byincluding keys when creating abranch, commit, or pull requestDismissSubtasksAdd subtaskDevelopment@ Open with VS Code |4 Create commit|Add linked work itemComponents~ ActivityAllComments HistorvJuoeroWork loaAdd a comment.Suggest a reply...Who ts workind on Ciis.status uooute.LabelsNoneStory point estimarNikolay YankovNikolay IvanovAneliya Angelovaalya Dimitrova ™4:14 PM | [Platform] Refinement ®Lukas Kovalik1:00:31Lộ3...
|
54523
|
NULL
|
NULL
|
NULL
|
|
54525
|
1896
|
53
|
2026-05-18T13:14:10.508557+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110050508_m1.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
6
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Unpin Nikolay Yankov's presentation from your main screen
You can't unmute someone else's presentation
More options for Nikolay Yankov
Zoom in
Open in new window
Enter Full Screen
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
Pin Nikolay Ivanov to your main screen
You can't unmute someone else
More options for Nikolay Ivanov
Nikolay Ivanov
Pin Aneliya Angelova to your main screen
You can't unmute someone else
More options for Aneliya Angelova
Aneliya Angelova
Pin Galya Dimitrova to your main screen
Mute Galya Dimitrova's microphone
More options for Galya Dimitrova
Galya Dimitrova
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
Others might see more of your background. Click to view your full video.
4:14
PM
[Platform] Refinement 🔍
[Platform] Refinement 🔍...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - [Platform] Refinement 🔍","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.8411111,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.0,"top":0.8794444,"width":0.033680554,"height":0.03888889},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.0,"top":0.91833335,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.95666665,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Nikolay Yankov (Presenting)","depth":12,"bounds":{"left":0.07534722,"top":0.101111114,"width":0.124305554,"height":0.022222223},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Yankov (Presenting)","depth":13,"bounds":{"left":0.07534722,"top":0.10222222,"width":0.124305554,"height":0.020555556},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.88680553,"top":0.08944444,"width":0.04097222,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":22,"bounds":{"left":0.9145833,"top":0.101111114,"width":0.0048611113,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Take notes with Gemini","depth":14,"bounds":{"left":0.93333334,"top":0.08944444,"width":0.025,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Take notes with Gemini","depth":17,"bounds":{"left":0.9361111,"top":0.101111114,"width":0.06388891,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Gemini","depth":17,"bounds":{"left":0.96666664,"top":0.101111114,"width":0.028125,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":17,"bounds":{"left":0.96458334,"top":0.090555556,"width":0.023611112,"height":0.037777778},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Unpin Nikolay Yankov's presentation from your main screen","depth":13,"bounds":{"left":0.34618056,"top":0.5088889,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else's presentation","depth":13,"bounds":{"left":0.37395832,"top":0.50666666,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Yankov","depth":13,"bounds":{"left":0.4045139,"top":0.5088889,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Zoom in","depth":13,"bounds":{"left":0.63090277,"top":0.78333336,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open in new window","depth":13,"bounds":{"left":0.6642361,"top":0.78333336,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Enter Full Screen","depth":13,"bounds":{"left":0.69756943,"top":0.78333336,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Pin Nikolay Yankov to your main screen","depth":13,"bounds":{"left":0.7607639,"top":0.25111112,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Mute Nikolay Yankov's microphone","depth":13,"bounds":{"left":0.7885417,"top":0.2488889,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Yankov","depth":13,"bounds":{"left":0.8190972,"top":0.25111112,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Nikolay Yankov","depth":17,"bounds":{"left":0.753125,"top":0.36277777,"width":0.07673611,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Nikolay Ivanov to your main screen","depth":13,"bounds":{"left":0.8871528,"top":0.25111112,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else","depth":13,"bounds":{"left":0.9149306,"top":0.2488889,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Ivanov","depth":13,"bounds":{"left":0.9454861,"top":0.25111112,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":17,"bounds":{"left":0.87951386,"top":0.36277777,"width":0.07395833,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Aneliya Angelova to your main screen","depth":13,"bounds":{"left":0.7607639,"top":0.5088889,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else","depth":13,"bounds":{"left":0.7885417,"top":0.50666666,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Aneliya Angelova","depth":13,"bounds":{"left":0.8190972,"top":0.5088889,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Aneliya Angelova","depth":17,"bounds":{"left":0.753125,"top":0.6205556,"width":0.088541664,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Galya Dimitrova to your main screen","depth":13,"bounds":{"left":0.8871528,"top":0.5088889,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Mute Galya Dimitrova's microphone","depth":13,"bounds":{"left":0.9149306,"top":0.50666666,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Galya Dimitrova","depth":13,"bounds":{"left":0.9454861,"top":0.5088889,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Galya Dimitrova","depth":17,"bounds":{"left":0.87951386,"top":0.6205556,"width":0.08194444,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"You’re continuously framed","depth":13,"bounds":{"left":0.82256943,"top":0.7644445,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Backgrounds and effects","depth":13,"bounds":{"left":0.853125,"top":0.7644445,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Lukas Kovalik","depth":13,"bounds":{"left":0.8836806,"top":0.76666665,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Lukas Kovalik","depth":17,"bounds":{"left":0.753125,"top":0.87833333,"width":0.06875,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Others might see more of your background. Click to view your full video.","depth":14,"bounds":{"left":0.96631944,"top":0.875,"width":0.018055556,"height":0.028888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4:14","depth":12,"bounds":{"left":0.050347224,"top":0.9444444,"width":0.021527778,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PM","depth":12,"bounds":{"left":0.07534722,"top":0.9444444,"width":0.016666668,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"[Platform] Refinement 🔍","depth":12,"bounds":{"left":0.109375,"top":0.9111111,"width":0.12881945,"height":0.08888888},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[Platform] Refinement 🔍","depth":15,"bounds":{"left":0.109375,"top":0.9438889,"width":0.12881945,"height":0.023333333},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
1031653575100856043
|
-537703529704335342
|
visual_change
|
hybrid
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
6
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Unpin Nikolay Yankov's presentation from your main screen
You can't unmute someone else's presentation
More options for Nikolay Yankov
Zoom in
Open in new window
Enter Full Screen
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
Pin Nikolay Ivanov to your main screen
You can't unmute someone else
More options for Nikolay Ivanov
Nikolay Ivanov
Pin Aneliya Angelova to your main screen
You can't unmute someone else
More options for Aneliya Angelova
Aneliya Angelova
Pin Galya Dimitrova to your main screen
Mute Galya Dimitrova's microphone
More options for Galya Dimitrova
Galya Dimitrova
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
Others might see more of your background. Click to view your full video.
4:14
PM
[Platform] Refinement 🔍
[Platform] Refinement 🔍
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp§ [Platform] Refinemen... 46 m left100% <8• Mon 18 May 16:14:10meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com=Nikolay Yankov (Presenting)+Mon 18 May 16:14(18® For youO Recent|# Starred8? Apps0 Spacesacarco(9 Service-DeskJiminny (Oid)Jiminny (New)1 CD Platform TeamCID Capture TeamIID Enterprise Stabllity L..(ID Processing TeamCID SE Kanban= More spaces= Filtersua basnooard€ Operations83 Confluence?: Teamt* Customise sidebarC Alte© Curs• Cos: | a0p C Alor | Pipe | Jmi Jm JimiL EY-Z UY-F Prol• smlhttps://fiminny.atlassian.net/browse/JY-20879C Projects|23 SSH• Home | Salesforce08 0© C OCca# Sentry *xL tasкinsights & Coachin…0 DevQ Search |+ CreateSpacesJminny (New)Add parent#JY-20679Enable users to use their new activity typesDescriptionCurrently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomatons that we have - this means they need to copy ar of theur prompes egan. wnich takes a nuge amouri or üre.For SF and Hubspot• when a newhctivity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to all the playbooks where activity type field ischenerFor SF, Hubspot, Copper, Bullhorn and Pipedrive:• if a customer manually creates an activity type in Jiminny - show them a warning message on the top saying "If you want this activity type to syne to your CRM oneach call - make sure to have ik in your CRM with the exact same name" - use this F https://www.figma.com/design/[XcUety9mxSFiz8KosLAUn/Project-Phoenix?node-id[PHONE]t•U/OZVbpzYIpCSPAy-1|Coet your Figma accos• the message should stay onty while they are adding the activity type - Il they leave or refresh the page it should disappear. Use the info message that we haveSubtasksAdd subtaskCinxeeWolknchnnAdd linked work item~ ActivityAllComments HistorvWork loaAdd a comment….Suggest a reply…..Wuo ts workind on tiis.status uooite.Ask RovoBackiog1 Improve StoryDetailsAssignee8 UnassignedAssign to meKOроrtEn2 Galya DimitrovaQuick start developmentLink this work item to your code byincluding keys when creating abranch, commit, or pull requestOismissDeveloomentl@ Open with VS Code |4 Create commit|ComponentsPlatformLabelsNoneStory point estimateNikolay YankovNikolay IvanoyAneliya AngelovaGalya DimitrovaTurn on microphone (g + d)4:14 PM | [Platform] Refinement ®Lukas Kovalik1:00:37...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
54527
|
1897
|
28
|
2026-05-18T13:14:12.640979+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110052640_m2.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
6...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - [Platform] Refinement 🔍","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.016123671,"height":-0.051875472},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.27094415,"top":1.0,"width":0.004986702,"height":-0.051875472},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.27310506,"top":1.0,"width":0.010638298,"height":-0.086193085},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Nikolay Yankov (Presenting)","depth":12,"bounds":{"left":0.30634972,"top":1.0,"width":0.059507977,"height":-0.072625756},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Yankov (Presenting)","depth":13,"bounds":{"left":0.30634972,"top":1.0,"width":0.059507977,"height":-0.07342374},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.69481385,"top":1.0,"width":0.019614361,"height":-0.06424582},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":22,"bounds":{"left":0.7081117,"top":1.0,"width":0.0023271276,"height":-0.072625756},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-5634609074044092938
|
4467700968764752518
|
click
|
hybrid
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
6
PnostormINavicatecodeFV faVsco.jsProiect© TranscriptionModelLocaleRepository.phplranscriptonkepository.onp© TranscriptionSummaryRepository.php© UserRepository.phpc vocabularykepository.onpv @ Rulescommaseparareckoles.ono© CrmReference.phpDependentRolesRule.phpc) DomainValidator.phpListenerRoleCannotHaveAdminOrManagerPermis® ListenerRoleRequiresListenerFeatureRule.phpc) MultidimensionalArravMaxcharRule.ohv@ NoinvitationsForEmailRule.ohn@ NoTeamMemberWithEmailRule.phpC) ParentNode.oho© RolesAndCrmOptionMismatchRule.php© SmsMessage.php© TransferUserAdminRule.phpvM Servicasv Activity>MAircalliM AmazonConnectD ApolloM AvavalD BaseServiceD Bloobirdsv O Closec cllentonoservice.ono> C CloudCall> D CloudTalk0 ConnectAndSelDemoDeskDialoadEichtsvziahta FiveNineGmailM GondGoodleMeetM GoToMeetingM GrooveD HubSpotM imnortD JiminnyMulustCal1M MiaratonM NatterhoymOfficoC OrumC7 Outreach© SoftPhoneManager.php(C) CoreUserRequest.onpscimProvistoning.ong© CoreUser.phpC service.php x© TextMessagingService.phpsyncrieldsx₽ Cc W .*saphpBSASI NIN3E2 AYnanespace Jumanny services accrvity closen› use ...class service excenas AccIVicyProviderservice 1mplements Imporcsuacalncertace28 6t31 6 >39 0>440)49 g )5401)87 0 >92 0 >107 0 >120 đ >prorecred scrina sname = ACCIVICV.:PKUVLUCKCLUSEYprivate webhook suruGenerator:public function -_construct(?array Sconfig)l...public function getbisplayName): stringt...public function getInstallationStrategy(): stringf...;public function canBeInstalled: booli...public function onActivityProviderIntegrated: voidf...public function onActivityProviderEnabled: voidt...hpublic function onActivityProviderDisabled: void{...}public function handleActivityProviderCallback(array $payload){...;public function supportedActivityChannels: array{...}* @throws SocialAccountTokenInvalidException*othrows_HttpBadrequestzxception*othrows HttpForb1ddenzxceptzon* othrows HttonotFoundExceptzon* othrows ouotazxceededexception* othrows ServiceUnavarLablezxceotion136 0oublic function importData(2Carbon $from = null. 2Carbon $to = null. 2Usen Suser = null. ?string &if (Sactivitvld)Sreauest'activity/call' Sactivitvid . !!.Sresponse = $this->client->request('GET', Srequest):= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]& console [EU]A console [STAGING]© CoachingFeedbackCoachUserln.php Xstohedeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 Ф >34 0 >45 đ >135136 @t›146 Gt151 C)1561usadeorivate const int No GROUP 10 = 9993 usagesorivate UserRenository SuserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection...hpublic function toArray: array{...hprivate function getOptions(): array{...}public function getValue: array{...}private function getDefaultValue: array{...}public function aetValidationRules(2strina Sorefix = null): arrav{...}public function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}40m lel• мon 10 May 10.14:14cascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSClM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for method9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ATTR_ ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabled•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/Ucer/M PoleAttr.nho 417ites/Userl ẞ RoleAttrTest.ohn +224* Reiect alliiAccent alliAsk anvthina (&4-L)« Code SWF-1.6157-27UTE.Afo 4 spaces...
|
54520
|
NULL
|
NULL
|
NULL
|
|
54526
|
1896
|
54
|
2026-05-18T13:14:12.658701+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110052658_m1.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
6
Take notes with Gemini
Take notes with Gemini...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - [Platform] Refinement 🔍","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.8411111,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.0,"top":0.8794444,"width":0.033680554,"height":0.03888889},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.0,"top":0.91833335,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.95666665,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Nikolay Yankov (Presenting)","depth":12,"bounds":{"left":0.07534722,"top":0.101111114,"width":0.124305554,"height":0.022222223},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Yankov (Presenting)","depth":13,"bounds":{"left":0.07534722,"top":0.10222222,"width":0.124305554,"height":0.020555556},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.88680553,"top":0.08944444,"width":0.04097222,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":22,"bounds":{"left":0.9145833,"top":0.101111114,"width":0.0048611113,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Take notes with Gemini","depth":14,"bounds":{"left":0.93333334,"top":0.08944444,"width":0.025,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Take notes with Gemini","depth":17,"bounds":{"left":0.9361111,"top":0.101111114,"width":0.06388891,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
4377558912522812948
|
9079391384635240583
|
click
|
hybrid
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
6
Take notes with Gemini
Take notes with Gemini
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp§ [Platform] Refinemen... 46 m left100% <8• Mon 18 May 16:14:12meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com+Nikolay Yankov (Presenting)EdtMon 18 May 16:14(18® For you© Recent|# Starred8? Apps02 SpacesSsarco(9 Service-DeskJiminny (Oid)Jiminny (New)I CD Ptatform TeamCID Capture TeamIID Enterprise Stabllity L...(ID Processing TeamCID SE Kanban= More spaces= FiltersB Dashboards€ OperationsE3 Confluence?8 Team2 Customise sidebanC AtteCurs• Com: 0 a0p C Alor Pipe O am Jm JemiLZ EY-Z UY-aIxF Prol0 sтihttps://fminny.atlassian.net/browse/JY-20879— ProgectsE3 SSH• Home| Salestorce O 8 E ElDatadog $ Claude Circiecia SentryL laskinsights & Coachin.C Dew+ CreateSpaces Jminny (Nrw)Add parert4JY-20679Enable users to use their new activity types~ DescriptionCurrently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomacons that we have - this means they need to copy ar or their prompes agan. wnich takes a nuge amourk or trmeFor SF and Hubspot:• when a new activity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to all the playbooks where activity type field isChenlesuFor SF, Hubspot, Copper, Bullhorn and Pipedrive:• il a customer manuatly creates an activity type in Jiminny - show them a warning message on the top saying "If you want this activity type to syne to your CRM oneach call - make sure to have ik in your CRM with the exact same name" - use this F https://www.figma.com/design/[XcUety9mxSFiz8KosLAUn/Project-Phoenix?node-id[PHONE]t•WJO2VbpzYIpCSPAy-1 |Coct your Figma accos• the message should stay only while they are adding the activity type - Il they leave or refresh the page it should disappear. Use the info message that we haveSubtasksAdd subtaskChxeeWolxiehsAdd linked work iter~ ActivityAllCommentsHistoryWork loaAdd a comment.Suggest a repty…..Wino ts workind on tiiseestatus upoaseAsk RovoBackiog1 Improve StoryDetailsAssignee8 UnassignedAssign to meКОРОRTR2 Galya DimitrovaQuick start developmentLink this work item to your code byincluding keys when creating abranch, commit, or pull requestbelow. Learn moreDismissDeveloomentl@ Open with VS Code |+ Create commi)ComponentsPlatformAdd optionsLabelsNoneStory point estimateNikolay YankovNikolay IvanovAneliya AngelovaGalya Dimitrova4:14 PM | [Platform] Refinement ®Lukas Kovalik1:00:39Lộ3...
|
54525
|
NULL
|
NULL
|
NULL
|
|
54528
|
1896
|
55
|
2026-05-18T13:14:14.778719+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110054778_m1.jpg...
|
PhpStorm
|
faVsco.js – Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4235983745889776938
|
-8204421443435123770
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp• 0• [Platform] Refinemen…46 m leftmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com8• Mon 18 May 16:14:14Nikolay Yankov (Presenting)$(18For you© Recent# Starred8? Apps02 Spacesauurco59 Service-DeskJiminny (Old)Jiminny (New)I CD Platform TeamIID Capture TeamID Enterprise Stabllity L..(ID Processing TeamCID SE Kanban= More spaces= Filtersua basnooardsOperations83 Confluence2: Team-* Customise sidebarC8 AlteOurs* Cau• Cos: a0p C Alor Pipe Jm Jmi Jmihttps://fiminny.atlassian.net/browse/JY-20879- JmemC Projects|• Home | Salestorce08dt 0@%COCcaa SentrySearchSpacesJminny (New)Add parentEnable users to use their new activity typesDescriptionCurrently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomacons that we have - this means they need to copy ar of their prompes agan. wnich takes a nuge amourk or ürmeFor SF and Hubspot:• when a new activity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to all the playbooks where activity type field ischeneruFor SF, Hubspot, Copper, Bullhorn and Pipedrive:• il a customer manually creates an activity type in Jiminny - show them a warning message on the top saying "If you want this activity type to syne to your CRM oneach call - make sure to have i in your CRM with the exact same name" - use this F https://www.figma.com/design/|XcUety9mxSFiz8KosLAUn/Project-Phoenix?node-id[PHONE]t•uJO2VbpzYIpCSPAy-1|Co•Flgma aoco• the message should stay only while they are adding the activity type - Il they leave or refresh the page it should disappear. Use the info message that we haveSubtasksAdd subtaskCinxeeWolknehnAdd linked work item~ ActivityAlCommentsHistorv.Work loaAdda comment.Suggest a repty...wnots workind on thisastatus upouteMon 18 May 16:142 tY-F Prol0 smlL TaskLa insights & Coachin0 Dev+ CreateAsk RovoBackiog* Improve Story~ DetailsAssignee8 UnassignedAssign to meКOрORTENg Galya DimitrovaQuick start developmentLink this work Item to your code byincluding keys when creating abranch, commit, or pull requestbelow. Learn moreDismissUewewoener@ Open with VS Code4 Create commiComponentsAdd optionsLabelsNone(0)Story point estimateNikolay YankovNikolay Ivanov00Aneliya AngelovaGalya Dimitrova4:14 PM | [Platform] Refinement ®Lukas Kovalik1:00:41...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
54529
|
1897
|
29
|
2026-05-18T13:14:19.625016+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110059625_m2.jpg...
|
PhpStorm
|
faVsco.js – Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormVIewINavigarecodeLaravelKeractorFV faVsco. PhpStormVIewINavigarecodeLaravelKeractorFV faVsco.js°9 master k >Proiect© SoftPhoneManager.php(C) CoreUserRequest.onpscimProvistoning.ong© CoreUser.php© TranscriptionModelLocaleRepository.phplranscriptonkepository.onpwch RoleAttrTest.ong© TranscriptionSummaryRepository.phpC service.php x© TextMessagingService.php© UserRepository.phpc vocabularykepository.onpsyncrieldsx₽ Cc W .*v @ Rulescommaseparareckoles.onosaphpBSAS1 NIN3S2 AY© CrmReference.phpDependentRolesRule.phpnaespace Jumanny services accrvity closenc) DomainValidator.php• use ...ListenerRoleCannotHaveAdminOrManagerPermis® ListenerRoleRequiresListenerFeatureRule.phpclass service excenas AccIVicyProviderservice 1mplements Imporcsuacalncertacec) MultidimensionalArravMaxcharRule.ohp@ NoinvitationsForEmailRule.ohn28 бt@ NoTeamMemberWithEmailRule.phpprorecred scrina sname = ACUIVICV.:PKUVLUCKCLUSEYC) ParentNode.ohoprivate webhook suruGenerator:© RolesAndCrmOptionMismatchRule.php© SmsMessage.php310 >public function -_construct(?array Sconfig)l...© TransferUserAdminRule.phpv M Servicas39 0>public function getbisplayName): stringt...v Activity>MAircalli440)M AmazonConnectpublic function getInstallationStrategy(): stringf...;D ApolloM Avaval49 )public function canBeInstalled: booli...D BaseServiceD Bloobirdsv D Closec cllentono5401)public function onActivityProviderIntegrated: voidf...87 0)public function onActivityProviderEnabled: voidt...service.ono> C CloudCall> D CloudTalk0 ConnectAndSel92 0 >public function onActivityProviderDisabled: void{...}107 G )public function handleActivityProviderCallback(array $payload){...;DemoDesk120 61 1public function supportedActivityChannels: array{...}DialoadEichtsvziahta FiveNineGmailM Gond* @throws SocialAccountTokenInvalidException*othrows_HttpBadrequestzxception*othrows HttpForb1ddenzxceptzonGoodleMeetM GoToMeeting* othrows HttonotFoundExceptzon* othrows ouotazxceededexceptionM GrooveD HubSpotM imnort* othrows ServiceUnavarLablezxceotion136 0oublic function importData(2Carbon $from = null. 2Carbon $to = null. ?Usen Susen = null. ?string &!D JiminnyMulustCalif (Sactivitvld) {1M Miaraton'activity/call' Sactivitvid . !!.M NatterhoymOfficoSresponse = $this->client->request('GET', Srequest):C Orum• Moutronch= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]A console [STAGING]© CoachingFeedbackCoachUserln.php Xstohedeclare(scrict_cypes=l)nnamespace Jiminny Component Activitysearch Filterdetin1t1on› use ...tinal class coachinoActions29 Ф >34 0 >45 đ >—119136 @t›146 @t>151 C)- 155ClassesFilesSymbols1usadeQ- Services/Crm/Close/Service.phporivate constir3 usagesC) Service.oho....apo/Services/Crm/Close/Service.phoC) ServiceTest.phn …/tests/Unit/Services/Crm/Close/ServiceTest.ohoorivate UserReod• Service.ono.him.ouild/coverade/Services/crm/closenublic functionpublic functionpublic functionpublic functionprivate functiorpubLic tunctionprivate tunctlornuouic tunctzonoublic functionoublic function& console [PROD]# console [euyProject Filesv* [Platform] Refinemen... 46 m left100% S2• Mon 18 May 16:14:19AskJiminnyReportActivityServiceTest vCascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSClM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for method9/ 10 tacke donea unit tests for SCIM role managementact Mario to vodate customermanagementtor leamtallor nas been successtully implemented. Here's what was completed:itation Summary:UserRequest DTO - Added role parsina from SCIM requestsConstants - Added META ROLES and USER_PATCH_ ATTR_ ROLES constantsAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations.Provisioning - Updated to handle roles during user creation with proper validationnerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromg admin/manager permissionsUser Response - Updated to include roles in SClM responsesTests - Created comprehensive tests for the validation rulemers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)ation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabledlner role cannot be combined with admin or manager permissionsexisting UpdateUserRolesAction for role syncing and loggingNS platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)): Please contact Mario to update the customer that SCIM role management is now available for TeamtailorOpen In Riaht Solitapp/Component/SClM/ Constants.php +3app/Component/SCIM/ ® ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/lcer/M RoleAttr.ohn t17ites/Userl ẞ RoleAttrTest.ohn +224Ask anvthina (&4-L)« Code SWF-1.6* Reiect alliiWN Windsurf Toams 00-6UTF.8View allAccent alliio 4 spaces...
|
NULL
|
8368896776815580464
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhpStormVIewINavigarecodeLaravelKeractorFV faVsco. PhpStormVIewINavigarecodeLaravelKeractorFV faVsco.js°9 master k >Proiect© SoftPhoneManager.php(C) CoreUserRequest.onpscimProvistoning.ong© CoreUser.php© TranscriptionModelLocaleRepository.phplranscriptonkepository.onpwch RoleAttrTest.ong© TranscriptionSummaryRepository.phpC service.php x© TextMessagingService.php© UserRepository.phpc vocabularykepository.onpsyncrieldsx₽ Cc W .*v @ Rulescommaseparareckoles.onosaphpBSAS1 NIN3S2 AY© CrmReference.phpDependentRolesRule.phpnaespace Jumanny services accrvity closenc) DomainValidator.php• use ...ListenerRoleCannotHaveAdminOrManagerPermis® ListenerRoleRequiresListenerFeatureRule.phpclass service excenas AccIVicyProviderservice 1mplements Imporcsuacalncertacec) MultidimensionalArravMaxcharRule.ohp@ NoinvitationsForEmailRule.ohn28 бt@ NoTeamMemberWithEmailRule.phpprorecred scrina sname = ACUIVICV.:PKUVLUCKCLUSEYC) ParentNode.ohoprivate webhook suruGenerator:© RolesAndCrmOptionMismatchRule.php© SmsMessage.php310 >public function -_construct(?array Sconfig)l...© TransferUserAdminRule.phpv M Servicas39 0>public function getbisplayName): stringt...v Activity>MAircalli440)M AmazonConnectpublic function getInstallationStrategy(): stringf...;D ApolloM Avaval49 )public function canBeInstalled: booli...D BaseServiceD Bloobirdsv D Closec cllentono5401)public function onActivityProviderIntegrated: voidf...87 0)public function onActivityProviderEnabled: voidt...service.ono> C CloudCall> D CloudTalk0 ConnectAndSel92 0 >public function onActivityProviderDisabled: void{...}107 G )public function handleActivityProviderCallback(array $payload){...;DemoDesk120 61 1public function supportedActivityChannels: array{...}DialoadEichtsvziahta FiveNineGmailM Gond* @throws SocialAccountTokenInvalidException*othrows_HttpBadrequestzxception*othrows HttpForb1ddenzxceptzonGoodleMeetM GoToMeeting* othrows HttonotFoundExceptzon* othrows ouotazxceededexceptionM GrooveD HubSpotM imnort* othrows ServiceUnavarLablezxceotion136 0oublic function importData(2Carbon $from = null. 2Carbon $to = null. ?Usen Susen = null. ?string &!D JiminnyMulustCalif (Sactivitvld) {1M Miaraton'activity/call' Sactivitvid . !!.M NatterhoymOfficoSresponse = $this->client->request('GET', Srequest):C Orum• Moutronch= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]A console [STAGING]© CoachingFeedbackCoachUserln.php Xstohedeclare(scrict_cypes=l)nnamespace Jiminny Component Activitysearch Filterdetin1t1on› use ...tinal class coachinoActions29 Ф >34 0 >45 đ >—119136 @t›146 @t>151 C)- 155ClassesFilesSymbols1usadeQ- Services/Crm/Close/Service.phporivate constir3 usagesC) Service.oho....apo/Services/Crm/Close/Service.phoC) ServiceTest.phn …/tests/Unit/Services/Crm/Close/ServiceTest.ohoorivate UserReod• Service.ono.him.ouild/coverade/Services/crm/closenublic functionpublic functionpublic functionpublic functionprivate functiorpubLic tunctionprivate tunctlornuouic tunctzonoublic functionoublic function& console [PROD]# console [euyProject Filesv* [Platform] Refinemen... 46 m left100% S2• Mon 18 May 16:14:19AskJiminnyReportActivityServiceTest vCascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSClM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for method9/ 10 tacke donea unit tests for SCIM role managementact Mario to vodate customermanagementtor leamtallor nas been successtully implemented. Here's what was completed:itation Summary:UserRequest DTO - Added role parsina from SCIM requestsConstants - Added META ROLES and USER_PATCH_ ATTR_ ROLES constantsAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations.Provisioning - Updated to handle roles during user creation with proper validationnerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromg admin/manager permissionsUser Response - Updated to include roles in SClM responsesTests - Created comprehensive tests for the validation rulemers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)ation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabledlner role cannot be combined with admin or manager permissionsexisting UpdateUserRolesAction for role syncing and loggingNS platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)): Please contact Mario to update the customer that SCIM role management is now available for TeamtailorOpen In Riaht Solitapp/Component/SClM/ Constants.php +3app/Component/SCIM/ ® ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/lcer/M RoleAttr.ohn t17ites/Userl ẞ RoleAttrTest.ohn +224Ask anvthina (&4-L)« Code SWF-1.6* Reiect alliiWN Windsurf Toams 00-6UTF.8View allAccent alliio 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
54531
|
1896
|
56
|
2026-05-18T13:14:20.887761+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110060887_m1.jpg...
|
PhpStorm
|
faVsco.js – Crm/…/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8043719072324535154
|
-8628527368849355612
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
FirefoxFileEditViewHistor Project: faVsco.js, menu
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp(Platform) Refinemen…46 m leftmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com8 • Mon 18 May 16:14:20Nikolay Yankov (Presenting)Mon 18 May 16:14$(18@ For you© Recent|# Starred8? Apps0 Spacesacurco(9 Service-DeskJiminny (Old)Jiminny (New)1 CD Platform TeamCID Capture TeamID Enterprise Stabllity L..(ID Processing TeamCID SE Kanban= More spaces= FiltersCB Dashboards€ Operations83 Confluence2: Team-* Customise sidebarC Alte© Ours* CiauCossaoou Alol3 Ppe© 3mi0 mhttps://fiminny.atlassian.net/browse/JY-20879C Projects|E SSH• Home | Salesforce0084Datadog1 Caud*xI+ CreateSpaces / LaJiminey (Nea)/, Add parere-JY-20879- DescriptionCurtently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomacons that we nave - tis means they need to copy at or their prompss egain. wruch taxes a nuge amourk or troe• whon a new activity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to all the playbooks where activity type field is• it a customer manuaty creates an activity type in Jiminny - show them a warning message on the top saying "Tl you wart this activity type to sync to your CRM oneach call - make sure to have ik in your CRM with the exact same name* - use this F https://www.figma.com/design/|XcUety9mx5Fiz8KosLAUn/Project-Phoenix?node-1d[PHONE]1•UJOZVbpzYIpC5PAy-1 [Connect your Figmy noccunl• the message should stay onty while they are adding the activity type - Il they leave or refresh the page it should disappear. Use the info message that we haveSubtasksAdd subtaskLinked work itemsAdd linied work item- ActivityHistoryWork logSuggest a repty...Who is working on this..?Status update..Proto? trontwdecorNixolay Yankov18 May 2026 x 13:42Questions:2 UY-F ProlInsights & Coachin- DetAsk RovoBackiog* Improve StoryDetailsAssignee& UnassignedAssign to meKOpOrtEn& Galya DimitrovaQuick start developmentLink this work tem to your code byincluding keys when creating abranch, commit, or pull requestbelow. Learn moreDismissCevelooment@ Open with VS Code|Create commlComponentsJuo-toAdd optionsLabelsNoneStory point estimateNikolay YankovNikolay IvanoyAneliya AngelovaGalya Dimitrova4:14 PM | [Platform] Refinement ®Lukas Kovalik1:00:47...
|
54528
|
NULL
|
NULL
|
NULL
|
|
54530
|
1897
|
30
|
2026-05-18T13:14:22.685567+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110062685_m2.jpg...
|
PhpStorm
|
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
sync
Inherited members (⌘R)
Anonymous Classes (⌘I) sync
Inherited members (⌘R)
Anonymous Classes (⌘I)
Lambdas (⌘L)
Service, class
syncAccount(crmId: string): Account|null ↑SyncCrmEntitiesInterface, public method...
|
[{"role":"AXTextField","text [{"role":"AXTextField","text":"sync","depth":1,"bounds":{"left":0.5212766,"top":0.31444532,"width":0.030917553,"height":0.022346368},"on_screen":true,"value":"sync","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Inherited members (⌘R)","depth":1,"bounds":{"left":0.5242686,"top":0.33998403,"width":0.052526597,"height":0.022346368},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Anonymous Classes (⌘I)","depth":1,"bounds":{"left":0.58011967,"top":0.33998403,"width":0.052526597,"height":0.022346368},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Lambdas (⌘L)","depth":1,"bounds":{"left":0.6359708,"top":0.33998403,"width":0.052526597,"height":0.022346368},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Service, class","depth":4,"bounds":{"left":0.5299202,"top":0.36472467,"width":0.023271276,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncAccount(crmId: string): Account|null ↑SyncCrmEntitiesInterface, public method","depth":5,"bounds":{"left":0.5362367,"top":0.38228253,"width":0.15458776,"height":0.017557861},"on_screen":true,"role_description":"text"}]...
|
3980676190014089882
|
938792179778450307
|
visual_change
|
accessibility
|
NULL
|
sync
Inherited members (⌘R)
Anonymous Classes (⌘I) sync
Inherited members (⌘R)
Anonymous Classes (⌘I)
Lambdas (⌘L)
Service, class
syncAccount(crmId: string): Account|null ↑SyncCrmEntitiesInterface, public method...
|
54529
|
NULL
|
NULL
|
NULL
|
|
54532
|
1896
|
57
|
2026-05-18T13:14:23.861451+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110063861_m1.jpg...
|
PhpStorm
|
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
sync
Inherited members (⌘R)
Anonymous Classes (⌘I) sync
Inherited members (⌘R)
Anonymous Classes (⌘I)
Lambdas (⌘L)
Service, class
syncAccount(crmId: string): Account|null ↑SyncCrmEntitiesInterface, public method
syncAccounts(since: \Carbon\Carbon, [to: \Carbon\Carbon|null = null]): int ↑SyncCrmEntitiesInterface, public method
syncContact(crmId: string): Contact|null ↑SyncCrmEntitiesInterface, public method
syncContacts(since: \Carbon\Carbon, [to: \Carbon\Carbon|null = null]): int ↑SyncCrmEntitiesInterface, public method
syncCustomFields(): void, private method
syncField(field: Field): void ↑SyncCrmEntitiesInterface, public method
syncFields(): void ↑SyncCrmEntitiesInterface, public method
syncLead(crmId: string): Lead|null ↑SyncCrmEntitiesInterface, public method
syncLeads(since: \Carbon\Carbon, [to: \Carbon\Carbon|null = null], [crmProfileId: null|string = null]): int ↑SyncCrmEntitiesInterface, public method
syncOpportunities(parameters: array, [strategy: null|string = null]): int ↑SyncCrmEntitiesInterface, public method
syncOpportunity(crmId: string): Opportunity|null ↑SyncCrmEntitiesInterface, public method
syncOrganization(): void ↑SyncCrmMetadataInterface, public method
syncProfileFields(): void ↑SyncCrmMetadataInterface, public method
syncProfiles([userToSearch: User|null = null]): Profile|null ↑SyncCrmMetadataInterface, public method
syncStandardFields(): void, private method
syncAccount(crmId: string): Account|null ↑SyncCrmEntitiesInterface, public method
syncAccounts(since: \Carbon\Carbon, [to: \Carbon\Carbon|null = null]): int ↑SyncCrmEntitiesInterface, public method
syncContact(crmId: string): Contact|null ↑SyncCrmEntitiesInterface, public method
syncContacts(since: \Carbon\Carbon, [to: \Carbon\Carbon|null = null]): int ↑SyncCrmEntitiesInterface, public method
syncCustomFields(): void, private method
syncField(field: Field): void ↑SyncCrmEntitiesInterface, public method
syncFields(): void ↑SyncCrmEntitiesInterface, public method
syncLead(crmId: string): Lead|null ↑SyncCrmEntitiesInterface, public method
syncLeads(since: \Carbon\Carbon, [to: \Carbon\Carbon|null = null], [crmProfileId: null|string = null]): int ↑SyncCrmEntitiesInterface, public method
syncOpportunities(parameters: array, [strategy: null|string = null]): int ↑SyncCrmEntitiesInterface, public method
syncOpportunity(crmId: string): Opportunity|null ↑SyncCrmEntitiesInterface, public method
syncOrganization(): void ↑SyncCrmMetadataInterface, public method
syncProfileFields(): void ↑SyncCrmMetadataInterface, public method
syncProfiles([userToSearch: User|null = null]): Profile|null ↑SyncCrmMetadataInterface, public method
syncStandardFields(): void, private method
Service.php...
|
[{"role":"AXTextField","text [{"role":"AXTextField","text":"sync","depth":1,"on_screen":true,"value":"sync","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Inherited members (⌘R)","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Anonymous Classes (⌘I)","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Lambdas (⌘L)","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Service, class","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncAccount(crmId: string): Account|null ↑SyncCrmEntitiesInterface, public method","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncAccounts(since: \\Carbon\\Carbon, [to: \\Carbon\\Carbon|null = null]): int ↑SyncCrmEntitiesInterface, public method","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncContact(crmId: string): Contact|null ↑SyncCrmEntitiesInterface, public method","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncContacts(since: \\Carbon\\Carbon, [to: \\Carbon\\Carbon|null = null]): int ↑SyncCrmEntitiesInterface, public method","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncCustomFields(): void, private method","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncField(field: Field): void ↑SyncCrmEntitiesInterface, public method","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncFields(): void ↑SyncCrmEntitiesInterface, public method","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncLead(crmId: string): Lead|null ↑SyncCrmEntitiesInterface, public method","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncLeads(since: \\Carbon\\Carbon, [to: \\Carbon\\Carbon|null = null], [crmProfileId: null|string = null]): int ↑SyncCrmEntitiesInterface, public method","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncOpportunities(parameters: array, [strategy: null|string = null]): int ↑SyncCrmEntitiesInterface, public method","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncOpportunity(crmId: string): Opportunity|null ↑SyncCrmEntitiesInterface, public method","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncOrganization(): void ↑SyncCrmMetadataInterface, public method","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncProfileFields(): void ↑SyncCrmMetadataInterface, public method","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncProfiles([userToSearch: User|null = null]): Profile|null ↑SyncCrmMetadataInterface, public method","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncStandardFields(): void, private method","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncAccount(crmId: string): Account|null ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncAccounts(since: \\Carbon\\Carbon, [to: \\Carbon\\Carbon|null = null]): int ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncContact(crmId: string): Contact|null ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncContacts(since: \\Carbon\\Carbon, [to: \\Carbon\\Carbon|null = null]): int ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncCustomFields(): void, private method","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncField(field: Field): void ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncFields(): void ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncLead(crmId: string): Lead|null ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncLeads(since: \\Carbon\\Carbon, [to: \\Carbon\\Carbon|null = null], [crmProfileId: null|string = null]): int ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncOpportunities(parameters: array, [strategy: null|string = null]): int ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncOpportunity(crmId: string): Opportunity|null ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncOrganization(): void ↑SyncCrmMetadataInterface, public method","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncProfileFields(): void ↑SyncCrmMetadataInterface, public method","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncProfiles([userToSearch: User|null = null]): Profile|null ↑SyncCrmMetadataInterface, public method","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncStandardFields(): void, private method","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Service.php","depth":1,"on_screen":true,"role_description":"text"}]...
|
5992398714886249002
|
6378599142419166703
|
visual_change
|
accessibility
|
NULL
|
sync
Inherited members (⌘R)
Anonymous Classes (⌘I) sync
Inherited members (⌘R)
Anonymous Classes (⌘I)
Lambdas (⌘L)
Service, class
syncAccount(crmId: string): Account|null ↑SyncCrmEntitiesInterface, public method
syncAccounts(since: \Carbon\Carbon, [to: \Carbon\Carbon|null = null]): int ↑SyncCrmEntitiesInterface, public method
syncContact(crmId: string): Contact|null ↑SyncCrmEntitiesInterface, public method
syncContacts(since: \Carbon\Carbon, [to: \Carbon\Carbon|null = null]): int ↑SyncCrmEntitiesInterface, public method
syncCustomFields(): void, private method
syncField(field: Field): void ↑SyncCrmEntitiesInterface, public method
syncFields(): void ↑SyncCrmEntitiesInterface, public method
syncLead(crmId: string): Lead|null ↑SyncCrmEntitiesInterface, public method
syncLeads(since: \Carbon\Carbon, [to: \Carbon\Carbon|null = null], [crmProfileId: null|string = null]): int ↑SyncCrmEntitiesInterface, public method
syncOpportunities(parameters: array, [strategy: null|string = null]): int ↑SyncCrmEntitiesInterface, public method
syncOpportunity(crmId: string): Opportunity|null ↑SyncCrmEntitiesInterface, public method
syncOrganization(): void ↑SyncCrmMetadataInterface, public method
syncProfileFields(): void ↑SyncCrmMetadataInterface, public method
syncProfiles([userToSearch: User|null = null]): Profile|null ↑SyncCrmMetadataInterface, public method
syncStandardFields(): void, private method
syncAccount(crmId: string): Account|null ↑SyncCrmEntitiesInterface, public method
syncAccounts(since: \Carbon\Carbon, [to: \Carbon\Carbon|null = null]): int ↑SyncCrmEntitiesInterface, public method
syncContact(crmId: string): Contact|null ↑SyncCrmEntitiesInterface, public method
syncContacts(since: \Carbon\Carbon, [to: \Carbon\Carbon|null = null]): int ↑SyncCrmEntitiesInterface, public method
syncCustomFields(): void, private method
syncField(field: Field): void ↑SyncCrmEntitiesInterface, public method
syncFields(): void ↑SyncCrmEntitiesInterface, public method
syncLead(crmId: string): Lead|null ↑SyncCrmEntitiesInterface, public method
syncLeads(since: \Carbon\Carbon, [to: \Carbon\Carbon|null = null], [crmProfileId: null|string = null]): int ↑SyncCrmEntitiesInterface, public method
syncOpportunities(parameters: array, [strategy: null|string = null]): int ↑SyncCrmEntitiesInterface, public method
syncOpportunity(crmId: string): Opportunity|null ↑SyncCrmEntitiesInterface, public method
syncOrganization(): void ↑SyncCrmMetadataInterface, public method
syncProfileFields(): void ↑SyncCrmMetadataInterface, public method
syncProfiles([userToSearch: User|null = null]): Profile|null ↑SyncCrmMetadataInterface, public method
syncStandardFields(): void, private method
Service.php...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
54533
|
1896
|
58
|
2026-05-18T13:14:26.719868+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110066719_m1.jpg...
|
PhpStorm
|
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
syncFiel
Inherited members (⌘R)
Anonymous Classes syncFiel
Inherited members (⌘R)
Anonymous Classes (⌘I)
Lambdas (⌘L)
Service, class
syncCustomFields(): void, private method
syncField(field: Field): void ↑SyncCrmEntitiesInterface, public method
syncFields(): void ↑SyncCrmEntitiesInterface, public method
syncProfileFields(): void ↑SyncCrmMetadataInterface, public method
syncStandardFields(): void, private method
syncAccount(crmId: string): Account|null ↑SyncCrmEntitiesInterface, public method
syncAccounts(since: \Carbon\Carbon, [to: \Carbon\Carbon|null = null]): int ↑SyncCrmEntitiesInterface, public method
syncContact(crmId: string): Contact|null ↑SyncCrmEntitiesInterface, public method
syncContacts(since: \Carbon\Carbon, [to: \Carbon\Carbon|null = null]): int ↑SyncCrmEntitiesInterface, public method
syncCustomFields(): void, private method
syncField(field: Field): void ↑SyncCrmEntitiesInterface, public method
syncFields(): void ↑SyncCrmEntitiesInterface, public method
syncLead(crmId: string): Lead|null ↑SyncCrmEntitiesInterface, public method
syncLeads(since: \Carbon\Carbon, [to: \Carbon\Carbon|null = null], [crmProfileId: null|string = null]): int ↑SyncCrmEntitiesInterface, public method
syncOpportunities(parameters: array, [strategy: null|string = null]): int ↑SyncCrmEntitiesInterface, public method...
|
[{"role":"AXTextField","text [{"role":"AXTextField","text":"syncFiel","depth":1,"on_screen":true,"value":"syncFiel","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Inherited members (⌘R)","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Anonymous Classes (⌘I)","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Lambdas (⌘L)","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Service, class","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncCustomFields(): void, private method","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncField(field: Field): void ↑SyncCrmEntitiesInterface, public method","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncFields(): void ↑SyncCrmEntitiesInterface, public method","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncProfileFields(): void ↑SyncCrmMetadataInterface, public method","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncStandardFields(): void, private method","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncAccount(crmId: string): Account|null ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"syncAccounts(since: \\Carbon\\Carbon, [to: \\Carbon\\Carbon|null = null]): int ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"syncContact(crmId: string): Contact|null ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"syncContacts(since: \\Carbon\\Carbon, [to: \\Carbon\\Carbon|null = null]): int ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"syncCustomFields(): void, private method","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncField(field: Field): void ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncFields(): void ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncLead(crmId: string): Lead|null ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"syncLeads(since: \\Carbon\\Carbon, [to: \\Carbon\\Carbon|null = null], [crmProfileId: null|string = null]): int ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"syncOpportunities(parameters: array, [strategy: null|string = null]): int ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":false,"role_description":"text"}]...
|
8365416367185537692
|
6378317804906566634
|
click
|
accessibility
|
NULL
|
syncFiel
Inherited members (⌘R)
Anonymous Classes syncFiel
Inherited members (⌘R)
Anonymous Classes (⌘I)
Lambdas (⌘L)
Service, class
syncCustomFields(): void, private method
syncField(field: Field): void ↑SyncCrmEntitiesInterface, public method
syncFields(): void ↑SyncCrmEntitiesInterface, public method
syncProfileFields(): void ↑SyncCrmMetadataInterface, public method
syncStandardFields(): void, private method
syncAccount(crmId: string): Account|null ↑SyncCrmEntitiesInterface, public method
syncAccounts(since: \Carbon\Carbon, [to: \Carbon\Carbon|null = null]): int ↑SyncCrmEntitiesInterface, public method
syncContact(crmId: string): Contact|null ↑SyncCrmEntitiesInterface, public method
syncContacts(since: \Carbon\Carbon, [to: \Carbon\Carbon|null = null]): int ↑SyncCrmEntitiesInterface, public method
syncCustomFields(): void, private method
syncField(field: Field): void ↑SyncCrmEntitiesInterface, public method
syncFields(): void ↑SyncCrmEntitiesInterface, public method
syncLead(crmId: string): Lead|null ↑SyncCrmEntitiesInterface, public method
syncLeads(since: \Carbon\Carbon, [to: \Carbon\Carbon|null = null], [crmProfileId: null|string = null]): int ↑SyncCrmEntitiesInterface, public method
syncOpportunities(parameters: array, [strategy: null|string = null]): int ↑SyncCrmEntitiesInterface, public method...
|
54532
|
NULL
|
NULL
|
NULL
|
|
54534
|
1897
|
31
|
2026-05-18T13:14:26.732112+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110066732_m2.jpg...
|
PhpStorm
|
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
syncFiel
Inherited members (⌘R)
Anonymous Classes syncFiel
Inherited members (⌘R)
Anonymous Classes (⌘I)
Lambdas (⌘L)
Service, class
syncCustomFields(): void, private method
syncField(field: Field): void ↑SyncCrmEntitiesInterface, public method
syncFields(): void ↑SyncCrmEntitiesInterface, public method
syncProfileFields(): void ↑SyncCrmMetadataInterface, public method
syncStandardFields(): void, private method
syncAccount(crmId: string): Account|null ↑SyncCrmEntitiesInterface, public method
syncAccounts(since: \Carbon\Carbon, [to: \Carbon\Carbon|null = null]): int ↑SyncCrmEntitiesInterface, public method
syncContact(crmId: string): Contact|null ↑SyncCrmEntitiesInterface, public method
syncContacts(since: \Carbon\Carbon, [to: \Carbon\Carbon|null = null]): int ↑SyncCrmEntitiesInterface, public method
syncCustomFields(): void, private method
syncField(field: Field): void ↑SyncCrmEntitiesInterface, public method
syncFields(): void ↑SyncCrmEntitiesInterface, public method
syncLead(crmId: string): Lead|null ↑SyncCrmEntitiesInterface, public method
syncLeads(since: \Carbon\Carbon, [to: \Carbon\Carbon|null = null], [crmProfileId: null|string = null]): int ↑SyncCrmEntitiesInterface, public method
syncOpportunities(parameters: array, [strategy: null|string = null]): int ↑SyncCrmEntitiesInterface, public method...
|
[{"role":"AXTextField","text [{"role":"AXTextField","text":"syncFiel","depth":1,"bounds":{"left":0.5212766,"top":0.31444532,"width":0.03324468,"height":0.022346368},"on_screen":true,"value":"syncFiel","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Inherited members (⌘R)","depth":1,"bounds":{"left":0.5242686,"top":0.33998403,"width":0.052526597,"height":0.022346368},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Anonymous Classes (⌘I)","depth":1,"bounds":{"left":0.58011967,"top":0.33998403,"width":0.052526597,"height":0.022346368},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Lambdas (⌘L)","depth":1,"bounds":{"left":0.6359708,"top":0.33998403,"width":0.052526597,"height":0.022346368},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Service, class","depth":4,"bounds":{"left":0.5299202,"top":0.36472467,"width":0.023271276,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncCustomFields(): void, private method","depth":5,"bounds":{"left":0.5362367,"top":0.38228253,"width":0.065159574,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncField(field: Field): void ↑SyncCrmEntitiesInterface, public method","depth":5,"bounds":{"left":0.5362367,"top":0.39984038,"width":0.1263298,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncFields(): void ↑SyncCrmEntitiesInterface, public method","depth":5,"bounds":{"left":0.5362367,"top":0.41739824,"width":0.107380316,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncProfileFields(): void ↑SyncCrmMetadataInterface, public method","depth":5,"bounds":{"left":0.5362367,"top":0.4349561,"width":0.125,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncStandardFields(): void, private method","depth":5,"bounds":{"left":0.5362367,"top":0.45251396,"width":0.06815159,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncAccount(crmId: string): Account|null ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"syncAccounts(since: \\Carbon\\Carbon, [to: \\Carbon\\Carbon|null = null]): int ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"syncContact(crmId: string): Contact|null ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"syncContacts(since: \\Carbon\\Carbon, [to: \\Carbon\\Carbon|null = null]): int ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"syncCustomFields(): void, private method","depth":4,"bounds":{"left":0.5362367,"top":0.38228253,"width":0.065159574,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncField(field: Field): void ↑SyncCrmEntitiesInterface, public method","depth":4,"bounds":{"left":0.5362367,"top":0.39984038,"width":0.1263298,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncFields(): void ↑SyncCrmEntitiesInterface, public method","depth":4,"bounds":{"left":0.5362367,"top":0.41739824,"width":0.107380316,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"syncLead(crmId: string): Lead|null ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"syncLeads(since: \\Carbon\\Carbon, [to: \\Carbon\\Carbon|null = null], [crmProfileId: null|string = null]): int ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"syncOpportunities(parameters: array, [strategy: null|string = null]): int ↑SyncCrmEntitiesInterface, public method","depth":4,"on_screen":false,"role_description":"text"}]...
|
8365416367185537692
|
6378317804906566634
|
click
|
accessibility
|
NULL
|
syncFiel
Inherited members (⌘R)
Anonymous Classes syncFiel
Inherited members (⌘R)
Anonymous Classes (⌘I)
Lambdas (⌘L)
Service, class
syncCustomFields(): void, private method
syncField(field: Field): void ↑SyncCrmEntitiesInterface, public method
syncFields(): void ↑SyncCrmEntitiesInterface, public method
syncProfileFields(): void ↑SyncCrmMetadataInterface, public method
syncStandardFields(): void, private method
syncAccount(crmId: string): Account|null ↑SyncCrmEntitiesInterface, public method
syncAccounts(since: \Carbon\Carbon, [to: \Carbon\Carbon|null = null]): int ↑SyncCrmEntitiesInterface, public method
syncContact(crmId: string): Contact|null ↑SyncCrmEntitiesInterface, public method
syncContacts(since: \Carbon\Carbon, [to: \Carbon\Carbon|null = null]): int ↑SyncCrmEntitiesInterface, public method
syncCustomFields(): void, private method
syncField(field: Field): void ↑SyncCrmEntitiesInterface, public method
syncFields(): void ↑SyncCrmEntitiesInterface, public method
syncLead(crmId: string): Lead|null ↑SyncCrmEntitiesInterface, public method
syncLeads(since: \Carbon\Carbon, [to: \Carbon\Carbon|null = null], [crmProfileId: null|string = null]): int ↑SyncCrmEntitiesInterface, public method
syncOpportunities(parameters: array, [strategy: null|string = null]): int ↑SyncCrmEntitiesInterface, public method...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
54535
|
1897
|
32
|
2026-05-18T13:14:28.738473+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110068738_m2.jpg...
|
PhpStorm
|
faVsco.js – Crm/…/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Close;
use Cache;
use Carbon\Carbon;
use Exception;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\CloseInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\ServiceUnavailableException;
use Jiminny\Exceptions\UnexpectedCallException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Close\Processor\AccountProcessor;
use Jiminny\Services\Crm\Close\Processor\MetadataProcessor;
use Jiminny\Services\Crm\Close\Processor\OpportunityProcessor;
use Jiminny\Services\Crm\Close\Processor\StageProcessor;
use Jiminny\Services\Crm\Helpers\FilterJoinedParticipants;
use Jiminny\Services\Crm\Metadata\OpportunityMetadata;
use Jiminny\Services\Crm\Metadata\ProfileMetadata;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Sentry;
use UnexpectedValueException;
class Service extends BaseService implements
CloseInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
RemoteEntityManipulationInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
SupportsObjectTypeParseInterface,
VerifyTaskExistsInterface
{
private const int NOTE_BODY_MAX_LENGTH = 3000000;
private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day
private StandardFieldMetadata $standardFieldMetadata;
private MetadataProcessor $metadataProcessor;
private FieldValueConverter $fieldValueConverter;
private StageProcessor $stageProcessor;
private OpportunityProcessor $opportunityProcessor;
private AccountProcessor $accountProcessor;
public function __construct(
Client $client,
StandardFieldMetadata $standardFieldMetadata,
MetadataProcessor $metadataProcessor,
FieldValueConverter $fieldValueConverter,
StageProcessor $stageResolver,
OpportunityProcessor $opportunityProcessor,
AccountProcessor $accountProcessor,
private readonly ProspectPhotoPathService $prospectPhotoPathService,
) {
parent::__construct();
$this->client = $client;
$this->standardFieldMetadata = $standardFieldMetadata;
$this->metadataProcessor = $metadataProcessor;
$this->fieldValueConverter = $fieldValueConverter;
$this->stageProcessor = $stageResolver;
$this->opportunityProcessor = $opportunityProcessor;
$this->accountProcessor = $accountProcessor;
}
public function getDisplayName(): string
{
return 'Close';
}
public function setConfiguration(Configuration $config): void
{
parent::setConfiguration($config);
$this->metadataProcessor->setConfiguration($config);
$this->stageProcessor->setConfiguration($config);
$this->opportunityProcessor->setConfiguration($config);
$this->accountProcessor->setConfiguration($config);
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
return $user->getTeam()->getOwner()->getSocialAccount(SocialAccount::PROVIDER_CLOSE);
}
private function getClient(): Client
{
if (! $this->client instanceof Client) {
throw new UnexpectedCallException('Client not set');
}
return $this->client;
}
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return $this->fieldValueConverter->convertFromCrm($fieldType, $fieldValue);
}
protected function getFieldTypes(): array
{
return [
parent::OBJECT_OPPORTUNITY,
parent::OBJECT_CONTACT,
parent::OBJECT_ACCOUNT,
];
}
protected function getFields(string $crmObject): array
{
// not used
return [];
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
// Set up the activity field as the default Type.
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'type',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
public function syncFields(): void
{
$this->syncStandardFields();
$this->syncCustomFields();
}
/**
* @important Works only for custom fields
*/
public function syncField(Field $field): void
{
$resource = $this->convertObjectTypeToResource($field->getObjectType());
// We can only sync custom fields in this CRM.
if ($this->isCustomField($field->getCrmProviderId()) === false) {
return;
}
$crmField = $this->getClient()->fetchCustomFieldDefinition($resource, $field->getCrmProviderId());
$this->metadataProcessor->syncField($crmField);
}
private function isCustomField(string $fieldId): bool
{
return strpos($fieldId, 'cf_') === 0;
}
/**
* @inheritdoc
*/
public function importPicklistValues(Field $field): array
{
// handled in syncFields()
return [];
}
/**
* @important We only support stages on the opportunity object
*
* @param string[]|null $types
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
if (! $missingStageName) {
// This is taken care of by syncOrganization()
return null;
}
$stage = $this->stageProcessor->resolveFromStageId($missingStageName);
if ($stage instanceof Stage) {
return $stage;
}
$stageMetadata = $this->getClient()->fetchStage($missingStageName);
if (! $stageMetadata) {
$this->logger->error('Stage does not exist', [
'stage' => $missingStageName,
]);
return null;
}
return $this->stageProcessor->importStage($stageMetadata);
}
/**
* @inheritdoc
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Even though Close.io has the concept of "leads", they fit more into our concept of accounts.
return 0;
}
/**
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Not a supported entity.
return null;
}
/**
* @throws Exception
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
foreach ($this->getClient()->listAccounts($since) as $clAccount) {
// Only sync if previously imported.
if ($this->hasAccount($clAccount->getId())) {
$this->importAccount($clAccount);
$syncCount++;
}
}
} catch (Exception $exception) {
$this->logger->error('Account sync failed', [
'error' => $exception->getMessage(),
]);
throw $exception;
}
return $syncCount;
}
public function syncAccount(string $crmId): ?Account
{
return $this->accountProcessor->syncAccount($crmId);
}
private function importAccount($crmData): Account
{
return $this->accountProcessor->importAccountMetadata($crmData);
}
/**
* @throws CloseException
*/
public function syncOpportunities(array $parameters, ?string $strategy = null): int
{
$strategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$strategies = $strategyResolver->getStrategies($this->config, $strategy);
$syncCount = 0;
try {
$opportunities = [];
foreach ($strategies as $syncStrategy) {
$opportunitiesData = $syncStrategy->fetchOpportunities($parameters);
$opportunities[] = $opportunitiesData['data'];
if ($opportunitiesData['has_more']) {
$this->logger->info('[Close] Sync Opportunities - count warning', [
'team_id' => $this->config->getTeam()->getId(),
'total' => $opportunitiesData['total'],
'count' => $opportunitiesData['count'],
'skip' => $opportunitiesData['skip'],
'strategies_count' => count($strategies),
]);
}
}
$opportunities = array_merge(...$opportunities);
} catch (CrmException $exception) {
$this->logger->error('Fetching opportunity data failed', [
'team' => $this->getTeam()->getSlug(),
'error' => $exception->getMessage(),
]);
return 0;
}
foreach ($opportunities as $opportunityMetadata) {
try {
$this->importOpportunity($opportunityMetadata);
$syncCount++;
} catch (Exception $exception) {
$this->logger->warning('Opportunity sync failed', [
'opportunity' => $opportunityMetadata->getId(),
'error' => $exception->getMessage(),
]);
}
}
return $syncCount;
}
public function syncOpportunity(string $crmId): ?Opportunity
{
$strategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$strategy = $strategyResolver->resolve(
$this->config,
OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY,
);
$parameters = ['crm_id' => $crmId];
$opportunity = $strategy->fetchOpportunities($parameters);
if (empty($opportunity['data'])) {
return null;
}
return $this->importOpportunity($opportunity['data']);
}
private function importOpportunity(OpportunityMetadata $crmData): ?Opportunity
{
if (! $crmData->getLeadId()) {
$this->logger->warning('Opportunity does not have a lead ID', [
'opportunity' => $crmData->getId(),
]);
return null;
}
$account = $this->getConfiguration()
->accounts()
->where('crm_provider_id', $crmData->getLeadId())
->first();
if ($account === null) {
$account = $this->accountProcessor->syncAccount($crmData->getLeadId());
}
/** @var Profile $profile */
$profile = $this->getConfiguration()
->profiles()
->where('crm_provider_id', $crmData->getUserId())
->first();
$userId = $profile?->getUserId() ?? $account?->getUserId();
if ($userId === null) {
$this->logger->error('[Close] | Skip import, no user_id found', [
'id' => $crmData->getId(),
]);
return null;
}
$stage = $this->getConfiguration()
->stages()
->where('crm_provider_id', $crmData->getStageId())
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $crmData->getStageId());
}
return $this->opportunityProcessor->import($crmData, $account, $stage, $profile);
}
/**
* @param array<string,string> $crmData
* @param string[] $crmFields
*/
public function importOpportunityCrmFieldData(array $crmData, array $crmFields, int $opportunityId): void
{
// handled in importOpportunity
}
/**
* @inheritdoc
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
/** No way to sync today.
$clContacts = $this->client->get('lead', [
'date_updated__gte' => $since->toDateString(),
'_order_by' => '-date_updated',
]);
foreach ($clContacts as $clContact) {
// Only sync if previously imported.
if ($this->hasContact($clContact['id'])) {
$this->importContact($clContact);
$syncCount++;
}
}
**/
} catch (Exception $exception) {
// Do nothing for now.
throw $exception;
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
$clContact = $this->client->get('contact/' . $crmId);
} catch (HttpNotFoundException $exception) {
return null;
}
return $this->importContact($clContact);
}
/**
* @inheritdoc
*/
private function importContact($crmData): Contact
{
$account = null;
if ($crmData['lead_id']) {
$account = $this->team
->accounts()
->where('crm_provider_id', $crmData['lead_id'])
->first();
if ($account === null) {
$account = $this->syncAccount($crmData['lead_id']);
}
}
$mobilePhone = $parsedNumber = null;
foreach ($crmData['phones'] as $phoneNumber) {
if ($phoneNumber['type'] === 'mobile') {
$mobilePhone = $this->buildContactMobilePhone(null, $phoneNumber['phone']);
} else {
$parsedNumber = $this->buildContactPhone(null, $phoneNumber['phone']);
}
}
$email = null;
if (empty($crmData['emails']) === false) {
$email = mb_strimwidth($crmData['emails'][0]['email'], 0, 80);
}
$profile = $this->config->profiles()->where('crm_provider_id', (string) $crmData['updated_by'])->first();
$data = [
'account_id' => $account->id ?? null,
'team_id' => $this->team->id,
'user_id' => $profile?->user_id,
'owner_id' => $crmData['updated_by'],
'name' => $crmData['name'] ?? 'Unknown',
'email' => $email,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobilePhone ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'title' => mb_strimwidth($crmData['title'] ?? '', 0, 128),
'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(
crmConfiguration: $this->config,
crmProviderId: $crmData['id'],
modelType: Contact::class,
fileName: $crmData['id'],
avatarText: $crmData['name'] ?? 'Unknown'
),
'remotely_created_at' => Carbon::parse($crmData['date_created']),
];
/** @var Contact */
return $this->config->contacts()->updateOrCreate(['crm_provider_id' => (string) $crmData['id']], $data);
}
private function buildContactPhone(?string $countryCode, ?string $number): ?array
{
if ($number) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($number, 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
return $parsedNumber;
}
private function buildContactMobilePhone(?string $countryCode, ?string $number): ?string
{
return $number ? mb_strimwidth(phone_e164($countryCode, $number), 0, 25) : null;
}
public function syncOrganization(): void
{
$organisation = $this->getClient()->fetchOrganisation();
$this->metadataProcessor->syncOrganisation($organisation);
foreach ($organisation->getPipelines() as $pipelineMetadata) {
$this->metadataProcessor->syncPipeline($pipelineMetadata);
}
}
private function syncStandardFields(): void
{
// Currently we sync only opportunity fields
$stages = $this->getClient()->listStages();
foreach ($this->standardFieldMetadata->opportunity($stages) as $fieldMetadata) {
$this->metadataProcessor->syncField($fieldMetadata);
}
$this->config->save();
}
private function syncCustomFields(): void
{
foreach ($this->getFieldTypes() as $fieldType) {
$objectType = $this->convertObjectTypeToResource($fieldType);
$currentFields = $this->getClient()->listCustomFieldDefinitions($objectType);
foreach ($currentFields as $fieldMetadata) {
$this->metadataProcessor->syncField($fieldMetadata);
}
}
$this->config->save();
}
public function syncProfiles(?User $userToSearch = null): ?Profile
{
/*
* Fetch the profile of the user from the database
* Then fetch the user metadata from Close and update it
* In case there's no profile for the user, proceed with syncing all users
*/
$foundUser = null;
if ($userToSearch) {
$profile = $userToSearch->getProfile();
if ($profile instanceof Profile) {
$crmProviderId = $profile->getCrmProviderId();
if ($crmProviderId) {
$profileMetadata = $this->getClient()->fetchUser($crmProviderId);
if (! $profileMetadata instanceof ProfileMetadata) {
return null;
}
return $this->metadataProcessor->syncProfile($profileMetadata);
}
}
}
foreach ($this->getClient()->listUsers() as $userMetadata) {
$userProfile = $this->metadataProcessor->syncProfile($userMetadata);
if (
$userToSearch instanceof User
&& $userProfile instanceof Profile
&& $userProfile->getUserId() === $userToSearch->getId()
) {
$foundUser = $userProfile;
}
}
return $foundUser;
}
public function syncProfileFields(): void
{
// Not used.
}
/**
* @inheritdoc
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
$data = Cache::remember($this->team->id . $name . $count . $offset, 300, function () use ($name, $count, $offset) {
$data = [];
try {
// If search phrase resembles phone number remove special symbols
if (preg_match('/^([0-9\s\-\+\(\)]*)$/', $name)) {
$name = '+' . preg_replace('/[\s\-\+\(\)]/', '', $name);
}
// Close do not provide a unified way to search, so we must hack our own.
$objects = $this->client->get('lead', [
'query' => 'sort:date_updated name:(' . $name . ') or email:(' . $name . ') or phone:(' . $name . ')',
'_limit' => $count, '_skip' => $offset,
]);
} catch (\GuzzleHttp\Exception\ServerException $exception) {
throw new ServiceUnavailableException($exception->getMessage());
}
foreach ($objects['data'] as $object) {
// We need a contact to dial it.
if (empty($object['contacts'])) {
continue;
}
foreach ($object['contacts'] as $contact) {
$record = [
'crmId' => $contact['id'],
'crmUrl' => $this->generateProviderUrl($object['id'], 'account'),
'name' => $contact['name'],
'industry' => null,
'title' => $contact['title'],
'organization' => $object['display_name'],
'prospectType' => 'contact',
'phoneNumbers' => [],
];
foreach ($contact['phones'] as $phone) {
if ($phone['type'] === 'mobile') {
$number = $this->buildContactMobilePhone(null, $phone['phone']);
$record['phoneNumbers'][] = [
'number' => $number,
'nationalFormat' => phone_national(null, $number),
'type' => 'mobile',
];
} else {
$parsedNumber = $this->buildContactPhone(null, $phone['phone']);
// Add phone number to record.
if (empty($parsedNumber['phone']) === false) {
$record['phoneNumbers'][] = [
'number' => $parsedNumber['phone'],
'nationalFormat' => phone_national(null, $parsedNumber['phone']),
'type' => 'phone',
];
}
}
}
$data[] = $record;
}
}
return $data;
});
return $data;
}
/**
* @inheritdoc
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
$contact = null;
$account = null;
if ($crmAccountId) {
$account = $this->config->accounts()->where('crm_provider_id', $crmAccountId)->first();
if ($account === null) {
$account = $this->syncAccount($crmAccountId);
}
}
if ($crmContactId) {
$contact = $this->config->contacts()->where('crm_provider_id', $crmContactId)->first();
if ($contact === null) {
$contact = $this->syncContact($crmContactId);
}
}
if ($contact || $account) {
if ($contact && $account === null) {
$account = $contact->account;
}
if ($account === null) {
return [];
}
$params = [
'lead_id' => $account->crm_provider_id,
'_order_by' => '-date_updated',
];
$onlyOpen = true;
switch ($this->config->opportunity_assignment_rule) {
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:
$params['_order_by'] = '-date_updated';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:
$params['_order_by'] = '-date_created';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:
$params['_order_by'] = 'date_created';
break;
case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:
$params['_order_by'] = '-date_updated';
$onlyOpen = false;
}
if ($onlyOpen) {
$params['status_type__in'] = 'active,won';
}
$clOpportunities = $this->client->get('opportunity', $params);
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
foreach ($clOpportunities['data'] as $clOpportunity) {
$stage = $this->config
->stages()
->where('crm_provider_id', $clOpportunity['status_id'])
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $clOpportunity['status_id']);
}
$record = [
'crmId' => $clOpportunity['id'],
'name' => $clOpportunity['note'],
'value' => currency_format($clOpportunity['value'], $clOpportunity['value_currency']),
'won' => $stage->probability === 100.00,
'closed' => $clOpportunity['status_type'] !== 'active',
'stage' => [
'id' => $stage->id_string,
'name' => $stage->name,
],
'recordType' => [],
];
if ($ownerId && isset($clOpportunity['user_id']) && $clOpportunity['user_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
$crmId = null;
if ($objectType === 'contact') {
$contact = $this->syncContact($objectId);
if ($contact && $contact->account_id) {
$crmId = $contact->account->crm_provider_id;
}
} else {
$crmId = $objectId;
}
if ($crmId) {
$clTasks = $this->client->get('task', [
'lead_id' => $crmId,
'_type' => 'lead',
'assigned_to' => $this->profile->crm_provider_id,
'is_complete' => 'false',
'_order_by' => 'date',
]);
foreach ($clTasks['data'] as $clTask) {
$data[] = [
'crmId' => $clTask['id'],
'subject' => $clTask['text'],
'due' => $clTask['date'] ?? null,
'type' => null,
];
}
}
return $data;
}
/**
* Try to find email address in CRM service
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$clObject = $this->client->get('lead', [
'query' => 'sort:updated contact(email(email:"' . $email . '"))',
'_limit' => 1,
]);
if ($clObject['total_results'] > 0) {
foreach ($clObject['data'][0]['contacts'] as $clContact) {
foreach ($clContact['emails'] as $clEmail) {
if ($email === $clEmail['email']) {
$data = $this->convertCrmData($clContact['id'], $userId);
return ! empty(array_filter($data)) ? $data : null;
}
}
}
}
return null;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
// Check if the user is internal.
$teamMember = $this->team->users()->where('phone', $phone)->exists();
// Skip the attendee if internal.
if ($teamMember === false) {
$clObject = $this->client->get('lead', [
'query' => 'sort:updated contact(' . $phone . ')',
'_limit' => 1,
]);
if ($clObject['total_results'] > 0) {
foreach ($clObject['data'][0]['contacts'] as $clContact) {
foreach ($clContact['phones'] as $clPhone) {
if ($phone === $clPhone['phone']) {
$data = $this->convertCrmData($clContact['id'], $userId);
return ! empty(array_filter($data)) ? $data : null;
}
}
}
}
}
return null;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$clObject = $this->client->get('lead', [
'query' => 'sort:updated contact(name:"' . $name . '")',
'_limit' => 1,
]);
if ($clObject['total_results'] > 0) {
foreach ($clObject['data'][0]['contacts'] as $clContact) {
if ($clContact['name'] === $name || $clContact['display_name'] === $name) {
$data = $this->convertCrmData($clContact['id'], $userId);
return ! empty(array_filter($data)) ? $data : false;
}
}
}
return false;
});
return is_array($result) ? $result : null;
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
private function convertCrmData(string $crmId, ?int $userId = null): array
{
$lead = null;
$opportunity = null;
$account = null;
$stage = null;
$countryCode = null;
$contact = $this->syncContact($crmId);
if ($contact) {
$account = $contact->account;
if ($contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account) {
$countryCode = $account->country_code;
}
try {
$cpOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId,
);
if (! empty($cpOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($cpOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception) {
// Nothing to see here.
}
}
return [
$lead,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
public function saveActivity(Activity $activity): Activity
{
switch ($activity->type) {
case Activity::TYPE_CONFERENCE:
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildCallPayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
private function mapStatus(string $status): string
{
switch ($status) {
case Activity::STATUS_COMPLETED:
case Activity::STATUS_IN_PROGRESS:
case Activity::STATUS_FAILED:
case Activity::STATUS_NO_ANSWER:
case Activity::STATUS_BUSY:
default:
return $status;
case Activity::STATUS_CANCELLED:
return 'cancel';
}
}
/**
* @throws CrmException
*/
private function buildCallPayload(Activity $activity): Activity
{
try {
if ($activity->crm_provider_id) {
// The activity should be logged under the existing Task (not Activity).
$data = [
'_type' => 'lead',
'lead_id' => $activity->account->crm_provider_id,
'assigned_to' => $this->profile->crm_provider_id,
'text' => $this->generateActivityDescription($activity),
'date' => $activity->getActualEndTime()->toDateString(),
'is_complete' => true,
];
$this->logger->info('[Close CRM] Updating task', [
'activity' => $activity->id,
'crm_id' => $activity->crm_provider_id,
'data' => $data,
]);
$this->client->put('task/' . $activity->crm_provider_id, $data);
} else {
// Just create an activity.
$data = [
'lead_id' => $activity->account_id ? $activity->account->crm_provider_id : null,
'contact_id' => $activity->contact_id ? $activity->contact->crm_provider_id : null,
'created_by' => $this->profile->crm_provider_id,
'user_id' => $this->profile->crm_provider_id,
'direction' => $activity->getCrmType() === Activity::TYPE_SOFTPHONE ? 'outbound' : 'inbound',
'status' => $this->mapStatus($activity->getStatus()),
'note' => $this->generateActivityDescription($activity),
'duration' => $this->mapStatus($activity->getStatus()) !== 'cancel' ? $activity->duration : 0,
'phone' => $activity->to ? $activity->to->phone_number : null,
];
$clActivity = $this->client->post('activity/call', $data);
$this->logger->info('[Close CRM] Creating activity', [
'activity' => $activity->id,
'crm_id' => $clActivity['id'],
'data' => $data,
'response' => $clActivity,
]);
$activity->crm_provider_id = $clActivity['id'];
$activity->save();
}
} catch (ClientException $exception) {
$response = $exception->getResponse();
if ($response === null) {
// Trying to debug weird cases where this is null.
Sentry::captureException($exception);
}
$responseBody = $response->getBody();
$message = $responseBody;
$errorCode = $response->getStatusCode();
$jsonResponse = json_decode($responseBody, true);
if (isset($jsonResponse[0]['message'])) {
$message = $jsonResponse[0]['message'];
}
throw new CrmException($message, $errorCode);
}
return $activity;
}
private function buildTextMessagePayload(Activity $activity): Activity
{
$clActivity = $this->client->post('activity/sms', [
'lead_id' => $activity->account_id ? $activity->account->crm_provider_id : null,
'contact_id' => $activity->contact_id ? $activity->contact->crm_provider_id : null,
'created_by' => $this->profile->crm_provider_id,
'user_id' => $this->profile->crm_provider_id,
'status' => $activity->getType() === Activity::TYPE_SMS_OUTBOUND ? 'sent' : 'inbox',
'text' => $this->generateActivityDescription($activity),
'remote_phone' => $activity->to ? $activity->to->phone_number : null,
'local_phone' => $activity->to ? $activity->to->phone_number : null,
'source' => 'Close.io',
]);
$activity->crm_provider_id = $clActivity['id'];
$activity->save();
return $activity;
}
private function generateActivityDescription(Activity $activity): string
{
$description = '';
switch ($activity->type) {
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
case Activity::TYPE_CONFERENCE:
if ($activity->hasActivityType()) {
$description = $activity->getActivityType()->getName() . PHP_EOL . PHP_EOL;
}
if ($activity->hasTitle()) {
$description .= $activity->getTitle() . PHP_EOL;
}
if ($activity->hasReasonCodeBotKicked()) {
$description .= 'Notetaker removed from this meeting' . PHP_EOL . PHP_EOL;
// When we fix the state to be Activity::RECORDING_RECORDED as it should be this can change.
} elseif ($activity->hasReasonCodeNotCompliant()) {
$description .= 'Notetaker did not join due to recording consent not being provided by attendees' . PHP_EOL . PHP_EOL;
} elseif ($activity->canReviewActivity()) {
$playbackUrl = $activity->user->team->partner->getPlaybackUrl($activity);
$description .= 'Review Activity:' . PHP_EOL . $playbackUrl . PHP_EOL . PHP_EOL;
}
if ($activity->type === Activity::TYPE_CONFERENCE) {
$description .= 'Attendees:'
. PHP_EOL
. (new FilterJoinedParticipants())->toString($activity);
}
if (\count($activity->notes) > 0) {
$description .= PHP_EOL . PHP_EOL . 'Notes: ' . PHP_EOL;
foreach ($activity->notes as $note) {
$time = ($note->time > 3600) ? gmdate('H:i:s', $note->time) : gmdate('i:s', $note->time);
$description .= $time . ' ' . $note->note . PHP_EOL;
}
}
// Get all private messages.
$messages = $activity->messages()
->where('is_private', 1)
->orderBy('created_at', 'asc');
if ($messages->count() > 0) {
$description .= PHP_EOL . PHP_EOL . 'Coaching Chat: ' . PHP_EOL;
foreach ($messages->get() as $message) {
$description .= $message->participant->name . ': ' . $message->message . PHP_EOL;
}
}
// Get all public messages.
$messages = $activity->messages()
->where('is_private', 0)
->orderBy('created_at', 'asc');
if ($messages->count() > 0) {
$description .= PHP_EOL . PHP_EOL . 'Customer Chat: ' . PHP_EOL;
foreach ($messages->get() as $message) {
$description .= $message->participant->name . ': ' . $message->message . PHP_EOL;
}
}
if ($activity->summary) {
$description .= PHP_EOL . PHP_EOL . 'Summary: ' . PHP_EOL . $activity->summary;
}
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$description = $activity->description;
break;
}
return $description;
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
// This is the user provided activity subject field.
if (empty($fields['name'])) {
return null;
}
$due = null;
if (empty($fields['due_date']) === false) {
$formatDue = Carbon::parse($fields['due_date']);
$due = $formatDue->toDateTimeString();
}
$clTask = $this->client->post('task', [
'_type' => 'lead',
'lead_id' => $activity->account->crm_provider_id,
'assigned_to' => $this->profile->crm_provider_id,
'text' => $fields['name'],
'date' => $due,
'is_complete' => false,
]);
// We don't actually create a corresponding activity object on our side yet.
return $clTask['id'];
}
/**
* Store transcripts as note.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
if ($activity->account_id === null) {
// We can only log to accounts (leads).
return;
}
// Generate activity transcription.
$transcriptionData = $this->generateTranscription($activity);
// Truncate Notes with max notes length because transcription text could be very long.
$transcripts = mb_strimwidth($transcriptionData, 0, self::NOTE_BODY_MAX_LENGTH);
$clActivity = $this->client->post('activity/note', [
'lead_id' => $activity->account_id ? $activity->account->crm_provider_id : null,
'created_by' => $this->profile->crm_provider_id,
'user_id' => $this->profile->crm_provider_id,
'note' => $transcripts,
]);
// Store CRM Activity ID in transcription.
$transcription = $activity->getTranscription();
$transcription->crm_activity_id = $clActivity['id'];
$transcription->save();
}
public function parseObjectType(string $objectId): string
{
if (Str::startsWith($objectId, 'lead')) {
return 'account';
}
if (Str::startsWith($objectId, 'cont')) {
return 'contact';
}
if (Str::startsWith($objectId, 'oppo')) {
return 'opportunity';
}
throw new InvalidArgumentException('Unsupported Object Type');
}
/**
* @inheritdoc
*/
public function updateStage($crmObject, Stage $stage): void
{
if ($crmObject instanceof Lead) {
// This would never get invoked since we merge lead/accounts in Close.
$this->client->put('lead/' . $crmObject->crm_provider_id, [
'status' => $stage->crm_provider_id,
]);
} else {
$this->client->put('opportunity/' . $crmObject->crm_provider_id, [
'status_id' => $stage->crm_provider_id,
]);
}
}
public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void
{
$this->getClient()->updateObject($this->convertObjectTypeToResource($objectType), $objectId, $data);
}
public function prepareValueForUpdate(array $params): array
{
$convertedValue = $this->fieldValueConverter->convertToCrm(
$this->config,
$params['fieldName'],
$params['fieldValue'],
);
if ($this->isCustomField($params['fieldName'])) {
$params['fieldName'] = 'custom.' . $params['fieldName'];
}
$params['fieldValue'] = $convertedValue;
return parent::prepareValueForUpdate($params);
}
public function getRecord(string $objectType, string $objectId, array $fields = []): array
{
return $this->client->get($this->convertObjectTypeToResource($objectType) . '/' . $objectId);
}
/**
*
* @throws UnexpectedValueException
*/
private function convertObjectTypeToResource(string $objectType): string
{
switch ($objectType) {
case FieldData::OBJECT_OPPORTUNITY:
return 'opportunity';
case FieldData::OBJECT_CONTACT:
return 'contact';
case FieldData::OBJECT_ACCOUNT:
return 'lead';
case FieldData::OBJECT_TASK:
return 'activity';
default:
throw new UnexpectedValueException('Unsupported object type "' . $objectType . '"');
}
}
public function generateProviderUrl(string $providerId, string $objectType): ?string
{
$baseUrl = 'https://app.close.com/';
$url = null;
switch ($objectType) {
case 'account':
$url = $baseUrl . 'lead/' . $providerId;
break;
case 'contact':
$contact = $this->config->contacts()->where('crm_provider_id', $providerId)->first();
if ($contact && $contact->account_id) {
$url = $baseUrl . 'lead/' . $contact->account->crm_provider_id;
}
break;
default:
// Sadly we can't deeplink to anything else in Close UI.
$url = null;
}
return $url;
}
/**
* Generate transcription for the activity.
*/
private function generateTranscription(Activity $activity): string
{
if (! $this->config->store_transcript) {
// If sending transcription to activity toggle is disabled
return '';
}
return $this->transcriptionService
->findTranscriptionByActivity($activity)
->map(static function (array $transcriptionSegment): string {
return $transcriptionSegment['formattedStartsAt'] . ' | ' . $transcriptionSegment['transcript'];
})
->implode(PHP_EOL);
}
public function verifyTaskExists(Activity $activity): bool
{
$crmProviderId = $activity->getCrmProviderId();
$cacheKey = "crm_task_exists:{$this->config->getId()}:$crmProviderId";
return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {
try {
$client = $this->getClient();
$task = $client->get('task/' . $crmProviderId);
return ! empty($task);
} catch (HttpNotFoundException) {
// Task not found in CRM - this is expected and permanent
$this->logger->info('[Close] Task not found during verification', [
'task_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.40226063,"top":0.17478053,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.17318435,"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.4192154,"top":0.17318435,"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\\Services\\Crm\\Close;\n\nuse Cache;\nuse Carbon\\Carbon;\nuse Exception;\nuse GuzzleHttp\\Exception\\ClientException;\nuse Illuminate\\Support\\Str;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\CloseInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\ServiceUnavailableException;\nuse Jiminny\\Exceptions\\UnexpectedCallException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Close\\Processor\\AccountProcessor;\nuse Jiminny\\Services\\Crm\\Close\\Processor\\MetadataProcessor;\nuse Jiminny\\Services\\Crm\\Close\\Processor\\OpportunityProcessor;\nuse Jiminny\\Services\\Crm\\Close\\Processor\\StageProcessor;\nuse Jiminny\\Services\\Crm\\Helpers\\FilterJoinedParticipants;\nuse Jiminny\\Services\\Crm\\Metadata\\OpportunityMetadata;\nuse Jiminny\\Services\\Crm\\Metadata\\ProfileMetadata;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Sentry;\nuse UnexpectedValueException;\n\nclass Service extends BaseService implements\n CloseInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n RemoteEntityManipulationInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n SupportsObjectTypeParseInterface,\n VerifyTaskExistsInterface\n{\n private const int NOTE_BODY_MAX_LENGTH = 3000000;\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n private StandardFieldMetadata $standardFieldMetadata;\n private MetadataProcessor $metadataProcessor;\n private FieldValueConverter $fieldValueConverter;\n private StageProcessor $stageProcessor;\n private OpportunityProcessor $opportunityProcessor;\n private AccountProcessor $accountProcessor;\n\n public function __construct(\n Client $client,\n StandardFieldMetadata $standardFieldMetadata,\n MetadataProcessor $metadataProcessor,\n FieldValueConverter $fieldValueConverter,\n StageProcessor $stageResolver,\n OpportunityProcessor $opportunityProcessor,\n AccountProcessor $accountProcessor,\n private readonly ProspectPhotoPathService $prospectPhotoPathService,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->standardFieldMetadata = $standardFieldMetadata;\n $this->metadataProcessor = $metadataProcessor;\n $this->fieldValueConverter = $fieldValueConverter;\n $this->stageProcessor = $stageResolver;\n $this->opportunityProcessor = $opportunityProcessor;\n $this->accountProcessor = $accountProcessor;\n }\n\n public function getDisplayName(): string\n {\n return 'Close';\n }\n\n public function setConfiguration(Configuration $config): void\n {\n parent::setConfiguration($config);\n\n $this->metadataProcessor->setConfiguration($config);\n $this->stageProcessor->setConfiguration($config);\n $this->opportunityProcessor->setConfiguration($config);\n $this->accountProcessor->setConfiguration($config);\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n return $user->getTeam()->getOwner()->getSocialAccount(SocialAccount::PROVIDER_CLOSE);\n }\n\n private function getClient(): Client\n {\n if (! $this->client instanceof Client) {\n throw new UnexpectedCallException('Client not set');\n }\n\n return $this->client;\n }\n\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return $this->fieldValueConverter->convertFromCrm($fieldType, $fieldValue);\n }\n\n protected function getFieldTypes(): array\n {\n return [\n parent::OBJECT_OPPORTUNITY,\n parent::OBJECT_CONTACT,\n parent::OBJECT_ACCOUNT,\n ];\n }\n\n protected function getFields(string $crmObject): array\n {\n // not used\n return [];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n // Set up the activity field as the default Type.\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'type',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n public function syncFields(): void\n {\n $this->syncStandardFields();\n $this->syncCustomFields();\n }\n\n /**\n * @important Works only for custom fields\n */\n public function syncField(Field $field): void\n {\n $resource = $this->convertObjectTypeToResource($field->getObjectType());\n\n // We can only sync custom fields in this CRM.\n if ($this->isCustomField($field->getCrmProviderId()) === false) {\n return;\n }\n\n $crmField = $this->getClient()->fetchCustomFieldDefinition($resource, $field->getCrmProviderId());\n\n $this->metadataProcessor->syncField($crmField);\n }\n\n private function isCustomField(string $fieldId): bool\n {\n return strpos($fieldId, 'cf_') === 0;\n }\n\n /**\n * @inheritdoc\n */\n public function importPicklistValues(Field $field): array\n {\n // handled in syncFields()\n return [];\n }\n\n /**\n * @important We only support stages on the opportunity object\n *\n * @param string[]|null $types\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n if (! $missingStageName) {\n // This is taken care of by syncOrganization()\n return null;\n }\n\n $stage = $this->stageProcessor->resolveFromStageId($missingStageName);\n\n if ($stage instanceof Stage) {\n return $stage;\n }\n\n $stageMetadata = $this->getClient()->fetchStage($missingStageName);\n\n if (! $stageMetadata) {\n $this->logger->error('Stage does not exist', [\n 'stage' => $missingStageName,\n ]);\n\n return null;\n }\n\n\n return $this->stageProcessor->importStage($stageMetadata);\n }\n\n /**\n * @inheritdoc\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Even though Close.io has the concept of \"leads\", they fit more into our concept of accounts.\n return 0;\n }\n\n /**\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Not a supported entity.\n return null;\n }\n\n /**\n * @throws Exception\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n foreach ($this->getClient()->listAccounts($since) as $clAccount) {\n // Only sync if previously imported.\n if ($this->hasAccount($clAccount->getId())) {\n $this->importAccount($clAccount);\n $syncCount++;\n }\n }\n } catch (Exception $exception) {\n $this->logger->error('Account sync failed', [\n 'error' => $exception->getMessage(),\n ]);\n\n throw $exception;\n }\n\n return $syncCount;\n }\n\n public function syncAccount(string $crmId): ?Account\n {\n return $this->accountProcessor->syncAccount($crmId);\n }\n\n private function importAccount($crmData): Account\n {\n return $this->accountProcessor->importAccountMetadata($crmData);\n }\n\n /**\n * @throws CloseException\n */\n public function syncOpportunities(array $parameters, ?string $strategy = null): int\n {\n $strategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $strategies = $strategyResolver->getStrategies($this->config, $strategy);\n\n $syncCount = 0;\n\n try {\n $opportunities = [];\n foreach ($strategies as $syncStrategy) {\n $opportunitiesData = $syncStrategy->fetchOpportunities($parameters);\n $opportunities[] = $opportunitiesData['data'];\n\n if ($opportunitiesData['has_more']) {\n $this->logger->info('[Close] Sync Opportunities - count warning', [\n 'team_id' => $this->config->getTeam()->getId(),\n 'total' => $opportunitiesData['total'],\n 'count' => $opportunitiesData['count'],\n 'skip' => $opportunitiesData['skip'],\n 'strategies_count' => count($strategies),\n ]);\n }\n }\n\n $opportunities = array_merge(...$opportunities);\n } catch (CrmException $exception) {\n $this->logger->error('Fetching opportunity data failed', [\n 'team' => $this->getTeam()->getSlug(),\n 'error' => $exception->getMessage(),\n ]);\n\n return 0;\n }\n\n foreach ($opportunities as $opportunityMetadata) {\n try {\n $this->importOpportunity($opportunityMetadata);\n $syncCount++;\n } catch (Exception $exception) {\n $this->logger->warning('Opportunity sync failed', [\n 'opportunity' => $opportunityMetadata->getId(),\n 'error' => $exception->getMessage(),\n ]);\n }\n }\n\n return $syncCount;\n }\n\n public function syncOpportunity(string $crmId): ?Opportunity\n {\n $strategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n\n $strategy = $strategyResolver->resolve(\n $this->config,\n OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY,\n );\n\n $parameters = ['crm_id' => $crmId];\n\n $opportunity = $strategy->fetchOpportunities($parameters);\n\n if (empty($opportunity['data'])) {\n return null;\n }\n\n return $this->importOpportunity($opportunity['data']);\n }\n\n private function importOpportunity(OpportunityMetadata $crmData): ?Opportunity\n {\n if (! $crmData->getLeadId()) {\n $this->logger->warning('Opportunity does not have a lead ID', [\n 'opportunity' => $crmData->getId(),\n ]);\n\n return null;\n }\n\n $account = $this->getConfiguration()\n ->accounts()\n ->where('crm_provider_id', $crmData->getLeadId())\n ->first();\n\n if ($account === null) {\n $account = $this->accountProcessor->syncAccount($crmData->getLeadId());\n }\n\n /** @var Profile $profile */\n $profile = $this->getConfiguration()\n ->profiles()\n ->where('crm_provider_id', $crmData->getUserId())\n ->first();\n\n $userId = $profile?->getUserId() ?? $account?->getUserId();\n if ($userId === null) {\n $this->logger->error('[Close] | Skip import, no user_id found', [\n 'id' => $crmData->getId(),\n ]);\n\n return null;\n }\n\n $stage = $this->getConfiguration()\n ->stages()\n ->where('crm_provider_id', $crmData->getStageId())\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $crmData->getStageId());\n }\n\n return $this->opportunityProcessor->import($crmData, $account, $stage, $profile);\n }\n\n /**\n * @param array<string,string> $crmData\n * @param string[] $crmFields\n */\n public function importOpportunityCrmFieldData(array $crmData, array $crmFields, int $opportunityId): void\n {\n // handled in importOpportunity\n }\n\n /**\n * @inheritdoc\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n /** No way to sync today.\n $clContacts = $this->client->get('lead', [\n 'date_updated__gte' => $since->toDateString(),\n '_order_by' => '-date_updated',\n ]);\n\n foreach ($clContacts as $clContact) {\n // Only sync if previously imported.\n if ($this->hasContact($clContact['id'])) {\n $this->importContact($clContact);\n $syncCount++;\n }\n }\n **/\n } catch (Exception $exception) {\n // Do nothing for now.\n throw $exception;\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n $clContact = $this->client->get('contact/' . $crmId);\n } catch (HttpNotFoundException $exception) {\n return null;\n }\n\n return $this->importContact($clContact);\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData): Contact\n {\n $account = null;\n if ($crmData['lead_id']) {\n $account = $this->team\n ->accounts()\n ->where('crm_provider_id', $crmData['lead_id'])\n ->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmData['lead_id']);\n }\n }\n\n $mobilePhone = $parsedNumber = null;\n foreach ($crmData['phones'] as $phoneNumber) {\n if ($phoneNumber['type'] === 'mobile') {\n $mobilePhone = $this->buildContactMobilePhone(null, $phoneNumber['phone']);\n } else {\n $parsedNumber = $this->buildContactPhone(null, $phoneNumber['phone']);\n }\n }\n\n $email = null;\n if (empty($crmData['emails']) === false) {\n $email = mb_strimwidth($crmData['emails'][0]['email'], 0, 80);\n }\n\n $profile = $this->config->profiles()->where('crm_provider_id', (string) $crmData['updated_by'])->first();\n\n $data = [\n 'account_id' => $account->id ?? null,\n 'team_id' => $this->team->id,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['updated_by'],\n 'name' => $crmData['name'] ?? 'Unknown',\n 'email' => $email,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobilePhone ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'title' => mb_strimwidth($crmData['title'] ?? '', 0, 128),\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['id'],\n modelType: Contact::class,\n fileName: $crmData['id'],\n avatarText: $crmData['name'] ?? 'Unknown'\n ),\n 'remotely_created_at' => Carbon::parse($crmData['date_created']),\n ];\n\n /** @var Contact */\n return $this->config->contacts()->updateOrCreate(['crm_provider_id' => (string) $crmData['id']], $data);\n }\n\n private function buildContactPhone(?string $countryCode, ?string $number): ?array\n {\n if ($number) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($number, 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n return $parsedNumber;\n }\n\n private function buildContactMobilePhone(?string $countryCode, ?string $number): ?string\n {\n return $number ? mb_strimwidth(phone_e164($countryCode, $number), 0, 25) : null;\n }\n\n public function syncOrganization(): void\n {\n $organisation = $this->getClient()->fetchOrganisation();\n\n $this->metadataProcessor->syncOrganisation($organisation);\n\n foreach ($organisation->getPipelines() as $pipelineMetadata) {\n $this->metadataProcessor->syncPipeline($pipelineMetadata);\n }\n }\n\n private function syncStandardFields(): void\n {\n // Currently we sync only opportunity fields\n $stages = $this->getClient()->listStages();\n foreach ($this->standardFieldMetadata->opportunity($stages) as $fieldMetadata) {\n $this->metadataProcessor->syncField($fieldMetadata);\n }\n\n $this->config->save();\n }\n\n private function syncCustomFields(): void\n {\n foreach ($this->getFieldTypes() as $fieldType) {\n $objectType = $this->convertObjectTypeToResource($fieldType);\n $currentFields = $this->getClient()->listCustomFieldDefinitions($objectType);\n\n foreach ($currentFields as $fieldMetadata) {\n $this->metadataProcessor->syncField($fieldMetadata);\n }\n }\n\n $this->config->save();\n }\n\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n /*\n * Fetch the profile of the user from the database\n * Then fetch the user metadata from Close and update it\n * In case there's no profile for the user, proceed with syncing all users\n */\n $foundUser = null;\n\n if ($userToSearch) {\n $profile = $userToSearch->getProfile();\n\n if ($profile instanceof Profile) {\n $crmProviderId = $profile->getCrmProviderId();\n\n if ($crmProviderId) {\n $profileMetadata = $this->getClient()->fetchUser($crmProviderId);\n\n if (! $profileMetadata instanceof ProfileMetadata) {\n return null;\n }\n\n return $this->metadataProcessor->syncProfile($profileMetadata);\n }\n }\n }\n\n foreach ($this->getClient()->listUsers() as $userMetadata) {\n $userProfile = $this->metadataProcessor->syncProfile($userMetadata);\n\n if (\n $userToSearch instanceof User\n && $userProfile instanceof Profile\n && $userProfile->getUserId() === $userToSearch->getId()\n ) {\n $foundUser = $userProfile;\n }\n }\n\n return $foundUser;\n }\n\n public function syncProfileFields(): void\n {\n // Not used.\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n $data = Cache::remember($this->team->id . $name . $count . $offset, 300, function () use ($name, $count, $offset) {\n $data = [];\n\n try {\n // If search phrase resembles phone number remove special symbols\n if (preg_match('/^([0-9\\s\\-\\+\\(\\)]*)$/', $name)) {\n $name = '+' . preg_replace('/[\\s\\-\\+\\(\\)]/', '', $name);\n }\n\n // Close do not provide a unified way to search, so we must hack our own.\n $objects = $this->client->get('lead', [\n 'query' => 'sort:date_updated name:(' . $name . ') or email:(' . $name . ') or phone:(' . $name . ')',\n '_limit' => $count, '_skip' => $offset,\n ]);\n } catch (\\GuzzleHttp\\Exception\\ServerException $exception) {\n throw new ServiceUnavailableException($exception->getMessage());\n }\n\n foreach ($objects['data'] as $object) {\n // We need a contact to dial it.\n if (empty($object['contacts'])) {\n continue;\n }\n\n foreach ($object['contacts'] as $contact) {\n $record = [\n 'crmId' => $contact['id'],\n 'crmUrl' => $this->generateProviderUrl($object['id'], 'account'),\n 'name' => $contact['name'],\n 'industry' => null,\n 'title' => $contact['title'],\n 'organization' => $object['display_name'],\n 'prospectType' => 'contact',\n 'phoneNumbers' => [],\n ];\n\n foreach ($contact['phones'] as $phone) {\n if ($phone['type'] === 'mobile') {\n $number = $this->buildContactMobilePhone(null, $phone['phone']);\n\n $record['phoneNumbers'][] = [\n 'number' => $number,\n 'nationalFormat' => phone_national(null, $number),\n 'type' => 'mobile',\n ];\n } else {\n $parsedNumber = $this->buildContactPhone(null, $phone['phone']);\n\n // Add phone number to record.\n if (empty($parsedNumber['phone']) === false) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national(null, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n }\n }\n\n $data[] = $record;\n }\n }\n\n return $data;\n });\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n $contact = null;\n $account = null;\n\n if ($crmAccountId) {\n $account = $this->config->accounts()->where('crm_provider_id', $crmAccountId)->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmAccountId);\n }\n }\n\n if ($crmContactId) {\n $contact = $this->config->contacts()->where('crm_provider_id', $crmContactId)->first();\n\n if ($contact === null) {\n $contact = $this->syncContact($crmContactId);\n }\n }\n\n if ($contact || $account) {\n if ($contact && $account === null) {\n $account = $contact->account;\n }\n\n if ($account === null) {\n return [];\n }\n\n $params = [\n 'lead_id' => $account->crm_provider_id,\n '_order_by' => '-date_updated',\n ];\n\n $onlyOpen = true;\n switch ($this->config->opportunity_assignment_rule) {\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:\n $params['_order_by'] = '-date_updated';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:\n $params['_order_by'] = '-date_created';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:\n $params['_order_by'] = 'date_created';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:\n $params['_order_by'] = '-date_updated';\n $onlyOpen = false;\n }\n\n if ($onlyOpen) {\n $params['status_type__in'] = 'active,won';\n }\n\n $clOpportunities = $this->client->get('opportunity', $params);\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n foreach ($clOpportunities['data'] as $clOpportunity) {\n $stage = $this->config\n ->stages()\n ->where('crm_provider_id', $clOpportunity['status_id'])\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $clOpportunity['status_id']);\n }\n\n $record = [\n 'crmId' => $clOpportunity['id'],\n 'name' => $clOpportunity['note'],\n 'value' => currency_format($clOpportunity['value'], $clOpportunity['value_currency']),\n 'won' => $stage->probability === 100.00,\n 'closed' => $clOpportunity['status_type'] !== 'active',\n 'stage' => [\n 'id' => $stage->id_string,\n 'name' => $stage->name,\n ],\n 'recordType' => [],\n ];\n\n if ($ownerId && isset($clOpportunity['user_id']) && $clOpportunity['user_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n $crmId = null;\n\n if ($objectType === 'contact') {\n $contact = $this->syncContact($objectId);\n\n if ($contact && $contact->account_id) {\n $crmId = $contact->account->crm_provider_id;\n }\n } else {\n $crmId = $objectId;\n }\n\n if ($crmId) {\n $clTasks = $this->client->get('task', [\n 'lead_id' => $crmId,\n '_type' => 'lead',\n 'assigned_to' => $this->profile->crm_provider_id,\n 'is_complete' => 'false',\n '_order_by' => 'date',\n ]);\n\n foreach ($clTasks['data'] as $clTask) {\n $data[] = [\n 'crmId' => $clTask['id'],\n 'subject' => $clTask['text'],\n 'due' => $clTask['date'] ?? null,\n 'type' => null,\n ];\n }\n }\n\n return $data;\n }\n\n /**\n * Try to find email address in CRM service\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $clObject = $this->client->get('lead', [\n 'query' => 'sort:updated contact(email(email:\"' . $email . '\"))',\n '_limit' => 1,\n ]);\n\n if ($clObject['total_results'] > 0) {\n foreach ($clObject['data'][0]['contacts'] as $clContact) {\n foreach ($clContact['emails'] as $clEmail) {\n if ($email === $clEmail['email']) {\n $data = $this->convertCrmData($clContact['id'], $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n }\n }\n }\n\n return null;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n // Check if the user is internal.\n $teamMember = $this->team->users()->where('phone', $phone)->exists();\n\n // Skip the attendee if internal.\n if ($teamMember === false) {\n $clObject = $this->client->get('lead', [\n 'query' => 'sort:updated contact(' . $phone . ')',\n '_limit' => 1,\n ]);\n\n if ($clObject['total_results'] > 0) {\n foreach ($clObject['data'][0]['contacts'] as $clContact) {\n foreach ($clContact['phones'] as $clPhone) {\n if ($phone === $clPhone['phone']) {\n $data = $this->convertCrmData($clContact['id'], $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n }\n }\n }\n }\n\n return null;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $clObject = $this->client->get('lead', [\n 'query' => 'sort:updated contact(name:\"' . $name . '\")',\n '_limit' => 1,\n ]);\n\n if ($clObject['total_results'] > 0) {\n foreach ($clObject['data'][0]['contacts'] as $clContact) {\n if ($clContact['name'] === $name || $clContact['display_name'] === $name) {\n $data = $this->convertCrmData($clContact['id'], $userId);\n\n return ! empty(array_filter($data)) ? $data : false;\n }\n }\n }\n\n return false;\n });\n\n return is_array($result) ? $result : null;\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n private function convertCrmData(string $crmId, ?int $userId = null): array\n {\n $lead = null;\n $opportunity = null;\n $account = null;\n $stage = null;\n $countryCode = null;\n\n $contact = $this->syncContact($crmId);\n if ($contact) {\n $account = $contact->account;\n\n if ($contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account) {\n $countryCode = $account->country_code;\n }\n\n try {\n $cpOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId,\n );\n\n if (! empty($cpOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($cpOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception) {\n // Nothing to see here.\n }\n }\n\n return [\n $lead,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->type) {\n case Activity::TYPE_CONFERENCE:\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildCallPayload($activity);\n\n break;\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n private function mapStatus(string $status): string\n {\n switch ($status) {\n case Activity::STATUS_COMPLETED:\n case Activity::STATUS_IN_PROGRESS:\n case Activity::STATUS_FAILED:\n case Activity::STATUS_NO_ANSWER:\n case Activity::STATUS_BUSY:\n default:\n return $status;\n case Activity::STATUS_CANCELLED:\n return 'cancel';\n }\n }\n\n /**\n * @throws CrmException\n */\n private function buildCallPayload(Activity $activity): Activity\n {\n try {\n if ($activity->crm_provider_id) {\n // The activity should be logged under the existing Task (not Activity).\n $data = [\n '_type' => 'lead',\n 'lead_id' => $activity->account->crm_provider_id,\n 'assigned_to' => $this->profile->crm_provider_id,\n 'text' => $this->generateActivityDescription($activity),\n 'date' => $activity->getActualEndTime()->toDateString(),\n 'is_complete' => true,\n ];\n\n $this->logger->info('[Close CRM] Updating task', [\n 'activity' => $activity->id,\n 'crm_id' => $activity->crm_provider_id,\n 'data' => $data,\n ]);\n\n $this->client->put('task/' . $activity->crm_provider_id, $data);\n } else {\n // Just create an activity.\n $data = [\n 'lead_id' => $activity->account_id ? $activity->account->crm_provider_id : null,\n 'contact_id' => $activity->contact_id ? $activity->contact->crm_provider_id : null,\n 'created_by' => $this->profile->crm_provider_id,\n 'user_id' => $this->profile->crm_provider_id,\n 'direction' => $activity->getCrmType() === Activity::TYPE_SOFTPHONE ? 'outbound' : 'inbound',\n 'status' => $this->mapStatus($activity->getStatus()),\n 'note' => $this->generateActivityDescription($activity),\n 'duration' => $this->mapStatus($activity->getStatus()) !== 'cancel' ? $activity->duration : 0,\n 'phone' => $activity->to ? $activity->to->phone_number : null,\n ];\n\n $clActivity = $this->client->post('activity/call', $data);\n\n $this->logger->info('[Close CRM] Creating activity', [\n 'activity' => $activity->id,\n 'crm_id' => $clActivity['id'],\n 'data' => $data,\n 'response' => $clActivity,\n ]);\n\n $activity->crm_provider_id = $clActivity['id'];\n $activity->save();\n }\n } catch (ClientException $exception) {\n $response = $exception->getResponse();\n\n if ($response === null) {\n // Trying to debug weird cases where this is null.\n Sentry::captureException($exception);\n }\n\n $responseBody = $response->getBody();\n $message = $responseBody;\n $errorCode = $response->getStatusCode();\n\n $jsonResponse = json_decode($responseBody, true);\n if (isset($jsonResponse[0]['message'])) {\n $message = $jsonResponse[0]['message'];\n }\n\n throw new CrmException($message, $errorCode);\n }\n\n return $activity;\n }\n\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $clActivity = $this->client->post('activity/sms', [\n 'lead_id' => $activity->account_id ? $activity->account->crm_provider_id : null,\n 'contact_id' => $activity->contact_id ? $activity->contact->crm_provider_id : null,\n 'created_by' => $this->profile->crm_provider_id,\n 'user_id' => $this->profile->crm_provider_id,\n 'status' => $activity->getType() === Activity::TYPE_SMS_OUTBOUND ? 'sent' : 'inbox',\n 'text' => $this->generateActivityDescription($activity),\n 'remote_phone' => $activity->to ? $activity->to->phone_number : null,\n 'local_phone' => $activity->to ? $activity->to->phone_number : null,\n 'source' => 'Close.io',\n ]);\n\n $activity->crm_provider_id = $clActivity['id'];\n $activity->save();\n\n return $activity;\n }\n\n private function generateActivityDescription(Activity $activity): string\n {\n $description = '';\n\n switch ($activity->type) {\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n case Activity::TYPE_CONFERENCE:\n if ($activity->hasActivityType()) {\n $description = $activity->getActivityType()->getName() . PHP_EOL . PHP_EOL;\n }\n if ($activity->hasTitle()) {\n $description .= $activity->getTitle() . PHP_EOL;\n }\n\n if ($activity->hasReasonCodeBotKicked()) {\n $description .= 'Notetaker removed from this meeting' . PHP_EOL . PHP_EOL;\n // When we fix the state to be Activity::RECORDING_RECORDED as it should be this can change.\n } elseif ($activity->hasReasonCodeNotCompliant()) {\n $description .= 'Notetaker did not join due to recording consent not being provided by attendees' . PHP_EOL . PHP_EOL;\n } elseif ($activity->canReviewActivity()) {\n $playbackUrl = $activity->user->team->partner->getPlaybackUrl($activity);\n $description .= 'Review Activity:' . PHP_EOL . $playbackUrl . PHP_EOL . PHP_EOL;\n }\n\n if ($activity->type === Activity::TYPE_CONFERENCE) {\n $description .= 'Attendees:'\n . PHP_EOL\n . (new FilterJoinedParticipants())->toString($activity);\n }\n\n if (\\count($activity->notes) > 0) {\n $description .= PHP_EOL . PHP_EOL . 'Notes: ' . PHP_EOL;\n\n foreach ($activity->notes as $note) {\n $time = ($note->time > 3600) ? gmdate('H:i:s', $note->time) : gmdate('i:s', $note->time);\n $description .= $time . ' ' . $note->note . PHP_EOL;\n }\n }\n\n // Get all private messages.\n $messages = $activity->messages()\n ->where('is_private', 1)\n ->orderBy('created_at', 'asc');\n\n if ($messages->count() > 0) {\n $description .= PHP_EOL . PHP_EOL . 'Coaching Chat: ' . PHP_EOL;\n\n foreach ($messages->get() as $message) {\n $description .= $message->participant->name . ': ' . $message->message . PHP_EOL;\n }\n }\n\n // Get all public messages.\n $messages = $activity->messages()\n ->where('is_private', 0)\n ->orderBy('created_at', 'asc');\n\n if ($messages->count() > 0) {\n $description .= PHP_EOL . PHP_EOL . 'Customer Chat: ' . PHP_EOL;\n\n foreach ($messages->get() as $message) {\n $description .= $message->participant->name . ': ' . $message->message . PHP_EOL;\n }\n }\n\n if ($activity->summary) {\n $description .= PHP_EOL . PHP_EOL . 'Summary: ' . PHP_EOL . $activity->summary;\n }\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $description = $activity->description;\n\n break;\n }\n\n return $description;\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n // This is the user provided activity subject field.\n if (empty($fields['name'])) {\n return null;\n }\n\n $due = null;\n if (empty($fields['due_date']) === false) {\n $formatDue = Carbon::parse($fields['due_date']);\n $due = $formatDue->toDateTimeString();\n }\n\n $clTask = $this->client->post('task', [\n '_type' => 'lead',\n 'lead_id' => $activity->account->crm_provider_id,\n 'assigned_to' => $this->profile->crm_provider_id,\n 'text' => $fields['name'],\n 'date' => $due,\n 'is_complete' => false,\n ]);\n\n // We don't actually create a corresponding activity object on our side yet.\n return $clTask['id'];\n }\n\n /**\n * Store transcripts as note.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n if ($activity->account_id === null) {\n // We can only log to accounts (leads).\n return;\n }\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, self::NOTE_BODY_MAX_LENGTH);\n\n $clActivity = $this->client->post('activity/note', [\n 'lead_id' => $activity->account_id ? $activity->account->crm_provider_id : null,\n 'created_by' => $this->profile->crm_provider_id,\n 'user_id' => $this->profile->crm_provider_id,\n 'note' => $transcripts,\n ]);\n\n // Store CRM Activity ID in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $clActivity['id'];\n $transcription->save();\n }\n\n public function parseObjectType(string $objectId): string\n {\n if (Str::startsWith($objectId, 'lead')) {\n return 'account';\n }\n\n if (Str::startsWith($objectId, 'cont')) {\n return 'contact';\n }\n\n if (Str::startsWith($objectId, 'oppo')) {\n return 'opportunity';\n }\n\n throw new InvalidArgumentException('Unsupported Object Type');\n }\n\n /**\n * @inheritdoc\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n if ($crmObject instanceof Lead) {\n // This would never get invoked since we merge lead/accounts in Close.\n $this->client->put('lead/' . $crmObject->crm_provider_id, [\n 'status' => $stage->crm_provider_id,\n ]);\n } else {\n $this->client->put('opportunity/' . $crmObject->crm_provider_id, [\n 'status_id' => $stage->crm_provider_id,\n ]);\n }\n }\n\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $this->getClient()->updateObject($this->convertObjectTypeToResource($objectType), $objectId, $data);\n }\n\n public function prepareValueForUpdate(array $params): array\n {\n $convertedValue = $this->fieldValueConverter->convertToCrm(\n $this->config,\n $params['fieldName'],\n $params['fieldValue'],\n );\n\n if ($this->isCustomField($params['fieldName'])) {\n $params['fieldName'] = 'custom.' . $params['fieldName'];\n }\n\n $params['fieldValue'] = $convertedValue;\n\n return parent::prepareValueForUpdate($params);\n }\n\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n return $this->client->get($this->convertObjectTypeToResource($objectType) . '/' . $objectId);\n }\n\n /**\n *\n * @throws UnexpectedValueException\n */\n private function convertObjectTypeToResource(string $objectType): string\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return 'opportunity';\n\n case FieldData::OBJECT_CONTACT:\n return 'contact';\n\n case FieldData::OBJECT_ACCOUNT:\n return 'lead';\n\n case FieldData::OBJECT_TASK:\n return 'activity';\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $baseUrl = 'https://app.close.com/';\n $url = null;\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'lead/' . $providerId;\n\n break;\n\n case 'contact':\n $contact = $this->config->contacts()->where('crm_provider_id', $providerId)->first();\n if ($contact && $contact->account_id) {\n $url = $baseUrl . 'lead/' . $contact->account->crm_provider_id;\n }\n\n break;\n\n default:\n // Sadly we can't deeplink to anything else in Close UI.\n $url = null;\n }\n\n return $url;\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n return $this->transcriptionService\n ->findTranscriptionByActivity($activity)\n ->map(static function (array $transcriptionSegment): string {\n return $transcriptionSegment['formattedStartsAt'] . ' | ' . $transcriptionSegment['transcript'];\n })\n ->implode(PHP_EOL);\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $client = $this->getClient();\n $task = $client->get('task/' . $crmProviderId);\n\n return ! empty($task);\n } catch (HttpNotFoundException) {\n // Task not found in CRM - this is expected and permanent\n $this->logger->info('[Close] Task not found during verification', [\n 'task_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n } catch (CloseException $e) {\n // Handle 404 responses from Close API\n if ($e->getResponseStatusCode() === 404) {\n $this->logger->info('[Close] Task not found during verification (404)', [\n 'task_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n\n // Re-throw other Close exceptions for retry\n throw $e;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Close;\n\nuse Cache;\nuse Carbon\\Carbon;\nuse Exception;\nuse GuzzleHttp\\Exception\\ClientException;\nuse Illuminate\\Support\\Str;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\CloseInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\ServiceUnavailableException;\nuse Jiminny\\Exceptions\\UnexpectedCallException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Close\\Processor\\AccountProcessor;\nuse Jiminny\\Services\\Crm\\Close\\Processor\\MetadataProcessor;\nuse Jiminny\\Services\\Crm\\Close\\Processor\\OpportunityProcessor;\nuse Jiminny\\Services\\Crm\\Close\\Processor\\StageProcessor;\nuse Jiminny\\Services\\Crm\\Helpers\\FilterJoinedParticipants;\nuse Jiminny\\Services\\Crm\\Metadata\\OpportunityMetadata;\nuse Jiminny\\Services\\Crm\\Metadata\\ProfileMetadata;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Sentry;\nuse UnexpectedValueException;\n\nclass Service extends BaseService implements\n CloseInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n RemoteEntityManipulationInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n SupportsObjectTypeParseInterface,\n VerifyTaskExistsInterface\n{\n private const int NOTE_BODY_MAX_LENGTH = 3000000;\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n private StandardFieldMetadata $standardFieldMetadata;\n private MetadataProcessor $metadataProcessor;\n private FieldValueConverter $fieldValueConverter;\n private StageProcessor $stageProcessor;\n private OpportunityProcessor $opportunityProcessor;\n private AccountProcessor $accountProcessor;\n\n public function __construct(\n Client $client,\n StandardFieldMetadata $standardFieldMetadata,\n MetadataProcessor $metadataProcessor,\n FieldValueConverter $fieldValueConverter,\n StageProcessor $stageResolver,\n OpportunityProcessor $opportunityProcessor,\n AccountProcessor $accountProcessor,\n private readonly ProspectPhotoPathService $prospectPhotoPathService,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->standardFieldMetadata = $standardFieldMetadata;\n $this->metadataProcessor = $metadataProcessor;\n $this->fieldValueConverter = $fieldValueConverter;\n $this->stageProcessor = $stageResolver;\n $this->opportunityProcessor = $opportunityProcessor;\n $this->accountProcessor = $accountProcessor;\n }\n\n public function getDisplayName(): string\n {\n return 'Close';\n }\n\n public function setConfiguration(Configuration $config): void\n {\n parent::setConfiguration($config);\n\n $this->metadataProcessor->setConfiguration($config);\n $this->stageProcessor->setConfiguration($config);\n $this->opportunityProcessor->setConfiguration($config);\n $this->accountProcessor->setConfiguration($config);\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n return $user->getTeam()->getOwner()->getSocialAccount(SocialAccount::PROVIDER_CLOSE);\n }\n\n private function getClient(): Client\n {\n if (! $this->client instanceof Client) {\n throw new UnexpectedCallException('Client not set');\n }\n\n return $this->client;\n }\n\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return $this->fieldValueConverter->convertFromCrm($fieldType, $fieldValue);\n }\n\n protected function getFieldTypes(): array\n {\n return [\n parent::OBJECT_OPPORTUNITY,\n parent::OBJECT_CONTACT,\n parent::OBJECT_ACCOUNT,\n ];\n }\n\n protected function getFields(string $crmObject): array\n {\n // not used\n return [];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n // Set up the activity field as the default Type.\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'type',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n public function syncFields(): void\n {\n $this->syncStandardFields();\n $this->syncCustomFields();\n }\n\n /**\n * @important Works only for custom fields\n */\n public function syncField(Field $field): void\n {\n $resource = $this->convertObjectTypeToResource($field->getObjectType());\n\n // We can only sync custom fields in this CRM.\n if ($this->isCustomField($field->getCrmProviderId()) === false) {\n return;\n }\n\n $crmField = $this->getClient()->fetchCustomFieldDefinition($resource, $field->getCrmProviderId());\n\n $this->metadataProcessor->syncField($crmField);\n }\n\n private function isCustomField(string $fieldId): bool\n {\n return strpos($fieldId, 'cf_') === 0;\n }\n\n /**\n * @inheritdoc\n */\n public function importPicklistValues(Field $field): array\n {\n // handled in syncFields()\n return [];\n }\n\n /**\n * @important We only support stages on the opportunity object\n *\n * @param string[]|null $types\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n if (! $missingStageName) {\n // This is taken care of by syncOrganization()\n return null;\n }\n\n $stage = $this->stageProcessor->resolveFromStageId($missingStageName);\n\n if ($stage instanceof Stage) {\n return $stage;\n }\n\n $stageMetadata = $this->getClient()->fetchStage($missingStageName);\n\n if (! $stageMetadata) {\n $this->logger->error('Stage does not exist', [\n 'stage' => $missingStageName,\n ]);\n\n return null;\n }\n\n\n return $this->stageProcessor->importStage($stageMetadata);\n }\n\n /**\n * @inheritdoc\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Even though Close.io has the concept of \"leads\", they fit more into our concept of accounts.\n return 0;\n }\n\n /**\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Not a supported entity.\n return null;\n }\n\n /**\n * @throws Exception\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n foreach ($this->getClient()->listAccounts($since) as $clAccount) {\n // Only sync if previously imported.\n if ($this->hasAccount($clAccount->getId())) {\n $this->importAccount($clAccount);\n $syncCount++;\n }\n }\n } catch (Exception $exception) {\n $this->logger->error('Account sync failed', [\n 'error' => $exception->getMessage(),\n ]);\n\n throw $exception;\n }\n\n return $syncCount;\n }\n\n public function syncAccount(string $crmId): ?Account\n {\n return $this->accountProcessor->syncAccount($crmId);\n }\n\n private function importAccount($crmData): Account\n {\n return $this->accountProcessor->importAccountMetadata($crmData);\n }\n\n /**\n * @throws CloseException\n */\n public function syncOpportunities(array $parameters, ?string $strategy = null): int\n {\n $strategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $strategies = $strategyResolver->getStrategies($this->config, $strategy);\n\n $syncCount = 0;\n\n try {\n $opportunities = [];\n foreach ($strategies as $syncStrategy) {\n $opportunitiesData = $syncStrategy->fetchOpportunities($parameters);\n $opportunities[] = $opportunitiesData['data'];\n\n if ($opportunitiesData['has_more']) {\n $this->logger->info('[Close] Sync Opportunities - count warning', [\n 'team_id' => $this->config->getTeam()->getId(),\n 'total' => $opportunitiesData['total'],\n 'count' => $opportunitiesData['count'],\n 'skip' => $opportunitiesData['skip'],\n 'strategies_count' => count($strategies),\n ]);\n }\n }\n\n $opportunities = array_merge(...$opportunities);\n } catch (CrmException $exception) {\n $this->logger->error('Fetching opportunity data failed', [\n 'team' => $this->getTeam()->getSlug(),\n 'error' => $exception->getMessage(),\n ]);\n\n return 0;\n }\n\n foreach ($opportunities as $opportunityMetadata) {\n try {\n $this->importOpportunity($opportunityMetadata);\n $syncCount++;\n } catch (Exception $exception) {\n $this->logger->warning('Opportunity sync failed', [\n 'opportunity' => $opportunityMetadata->getId(),\n 'error' => $exception->getMessage(),\n ]);\n }\n }\n\n return $syncCount;\n }\n\n public function syncOpportunity(string $crmId): ?Opportunity\n {\n $strategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n\n $strategy = $strategyResolver->resolve(\n $this->config,\n OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY,\n );\n\n $parameters = ['crm_id' => $crmId];\n\n $opportunity = $strategy->fetchOpportunities($parameters);\n\n if (empty($opportunity['data'])) {\n return null;\n }\n\n return $this->importOpportunity($opportunity['data']);\n }\n\n private function importOpportunity(OpportunityMetadata $crmData): ?Opportunity\n {\n if (! $crmData->getLeadId()) {\n $this->logger->warning('Opportunity does not have a lead ID', [\n 'opportunity' => $crmData->getId(),\n ]);\n\n return null;\n }\n\n $account = $this->getConfiguration()\n ->accounts()\n ->where('crm_provider_id', $crmData->getLeadId())\n ->first();\n\n if ($account === null) {\n $account = $this->accountProcessor->syncAccount($crmData->getLeadId());\n }\n\n /** @var Profile $profile */\n $profile = $this->getConfiguration()\n ->profiles()\n ->where('crm_provider_id', $crmData->getUserId())\n ->first();\n\n $userId = $profile?->getUserId() ?? $account?->getUserId();\n if ($userId === null) {\n $this->logger->error('[Close] | Skip import, no user_id found', [\n 'id' => $crmData->getId(),\n ]);\n\n return null;\n }\n\n $stage = $this->getConfiguration()\n ->stages()\n ->where('crm_provider_id', $crmData->getStageId())\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $crmData->getStageId());\n }\n\n return $this->opportunityProcessor->import($crmData, $account, $stage, $profile);\n }\n\n /**\n * @param array<string,string> $crmData\n * @param string[] $crmFields\n */\n public function importOpportunityCrmFieldData(array $crmData, array $crmFields, int $opportunityId): void\n {\n // handled in importOpportunity\n }\n\n /**\n * @inheritdoc\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n /** No way to sync today.\n $clContacts = $this->client->get('lead', [\n 'date_updated__gte' => $since->toDateString(),\n '_order_by' => '-date_updated',\n ]);\n\n foreach ($clContacts as $clContact) {\n // Only sync if previously imported.\n if ($this->hasContact($clContact['id'])) {\n $this->importContact($clContact);\n $syncCount++;\n }\n }\n **/\n } catch (Exception $exception) {\n // Do nothing for now.\n throw $exception;\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n $clContact = $this->client->get('contact/' . $crmId);\n } catch (HttpNotFoundException $exception) {\n return null;\n }\n\n return $this->importContact($clContact);\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData): Contact\n {\n $account = null;\n if ($crmData['lead_id']) {\n $account = $this->team\n ->accounts()\n ->where('crm_provider_id', $crmData['lead_id'])\n ->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmData['lead_id']);\n }\n }\n\n $mobilePhone = $parsedNumber = null;\n foreach ($crmData['phones'] as $phoneNumber) {\n if ($phoneNumber['type'] === 'mobile') {\n $mobilePhone = $this->buildContactMobilePhone(null, $phoneNumber['phone']);\n } else {\n $parsedNumber = $this->buildContactPhone(null, $phoneNumber['phone']);\n }\n }\n\n $email = null;\n if (empty($crmData['emails']) === false) {\n $email = mb_strimwidth($crmData['emails'][0]['email'], 0, 80);\n }\n\n $profile = $this->config->profiles()->where('crm_provider_id', (string) $crmData['updated_by'])->first();\n\n $data = [\n 'account_id' => $account->id ?? null,\n 'team_id' => $this->team->id,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['updated_by'],\n 'name' => $crmData['name'] ?? 'Unknown',\n 'email' => $email,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobilePhone ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'title' => mb_strimwidth($crmData['title'] ?? '', 0, 128),\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['id'],\n modelType: Contact::class,\n fileName: $crmData['id'],\n avatarText: $crmData['name'] ?? 'Unknown'\n ),\n 'remotely_created_at' => Carbon::parse($crmData['date_created']),\n ];\n\n /** @var Contact */\n return $this->config->contacts()->updateOrCreate(['crm_provider_id' => (string) $crmData['id']], $data);\n }\n\n private function buildContactPhone(?string $countryCode, ?string $number): ?array\n {\n if ($number) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($number, 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n return $parsedNumber;\n }\n\n private function buildContactMobilePhone(?string $countryCode, ?string $number): ?string\n {\n return $number ? mb_strimwidth(phone_e164($countryCode, $number), 0, 25) : null;\n }\n\n public function syncOrganization(): void\n {\n $organisation = $this->getClient()->fetchOrganisation();\n\n $this->metadataProcessor->syncOrganisation($organisation);\n\n foreach ($organisation->getPipelines() as $pipelineMetadata) {\n $this->metadataProcessor->syncPipeline($pipelineMetadata);\n }\n }\n\n private function syncStandardFields(): void\n {\n // Currently we sync only opportunity fields\n $stages = $this->getClient()->listStages();\n foreach ($this->standardFieldMetadata->opportunity($stages) as $fieldMetadata) {\n $this->metadataProcessor->syncField($fieldMetadata);\n }\n\n $this->config->save();\n }\n\n private function syncCustomFields(): void\n {\n foreach ($this->getFieldTypes() as $fieldType) {\n $objectType = $this->convertObjectTypeToResource($fieldType);\n $currentFields = $this->getClient()->listCustomFieldDefinitions($objectType);\n\n foreach ($currentFields as $fieldMetadata) {\n $this->metadataProcessor->syncField($fieldMetadata);\n }\n }\n\n $this->config->save();\n }\n\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n /*\n * Fetch the profile of the user from the database\n * Then fetch the user metadata from Close and update it\n * In case there's no profile for the user, proceed with syncing all users\n */\n $foundUser = null;\n\n if ($userToSearch) {\n $profile = $userToSearch->getProfile();\n\n if ($profile instanceof Profile) {\n $crmProviderId = $profile->getCrmProviderId();\n\n if ($crmProviderId) {\n $profileMetadata = $this->getClient()->fetchUser($crmProviderId);\n\n if (! $profileMetadata instanceof ProfileMetadata) {\n return null;\n }\n\n return $this->metadataProcessor->syncProfile($profileMetadata);\n }\n }\n }\n\n foreach ($this->getClient()->listUsers() as $userMetadata) {\n $userProfile = $this->metadataProcessor->syncProfile($userMetadata);\n\n if (\n $userToSearch instanceof User\n && $userProfile instanceof Profile\n && $userProfile->getUserId() === $userToSearch->getId()\n ) {\n $foundUser = $userProfile;\n }\n }\n\n return $foundUser;\n }\n\n public function syncProfileFields(): void\n {\n // Not used.\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n $data = Cache::remember($this->team->id . $name . $count . $offset, 300, function () use ($name, $count, $offset) {\n $data = [];\n\n try {\n // If search phrase resembles phone number remove special symbols\n if (preg_match('/^([0-9\\s\\-\\+\\(\\)]*)$/', $name)) {\n $name = '+' . preg_replace('/[\\s\\-\\+\\(\\)]/', '', $name);\n }\n\n // Close do not provide a unified way to search, so we must hack our own.\n $objects = $this->client->get('lead', [\n 'query' => 'sort:date_updated name:(' . $name . ') or email:(' . $name . ') or phone:(' . $name . ')',\n '_limit' => $count, '_skip' => $offset,\n ]);\n } catch (\\GuzzleHttp\\Exception\\ServerException $exception) {\n throw new ServiceUnavailableException($exception->getMessage());\n }\n\n foreach ($objects['data'] as $object) {\n // We need a contact to dial it.\n if (empty($object['contacts'])) {\n continue;\n }\n\n foreach ($object['contacts'] as $contact) {\n $record = [\n 'crmId' => $contact['id'],\n 'crmUrl' => $this->generateProviderUrl($object['id'], 'account'),\n 'name' => $contact['name'],\n 'industry' => null,\n 'title' => $contact['title'],\n 'organization' => $object['display_name'],\n 'prospectType' => 'contact',\n 'phoneNumbers' => [],\n ];\n\n foreach ($contact['phones'] as $phone) {\n if ($phone['type'] === 'mobile') {\n $number = $this->buildContactMobilePhone(null, $phone['phone']);\n\n $record['phoneNumbers'][] = [\n 'number' => $number,\n 'nationalFormat' => phone_national(null, $number),\n 'type' => 'mobile',\n ];\n } else {\n $parsedNumber = $this->buildContactPhone(null, $phone['phone']);\n\n // Add phone number to record.\n if (empty($parsedNumber['phone']) === false) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national(null, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n }\n }\n\n $data[] = $record;\n }\n }\n\n return $data;\n });\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n $contact = null;\n $account = null;\n\n if ($crmAccountId) {\n $account = $this->config->accounts()->where('crm_provider_id', $crmAccountId)->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmAccountId);\n }\n }\n\n if ($crmContactId) {\n $contact = $this->config->contacts()->where('crm_provider_id', $crmContactId)->first();\n\n if ($contact === null) {\n $contact = $this->syncContact($crmContactId);\n }\n }\n\n if ($contact || $account) {\n if ($contact && $account === null) {\n $account = $contact->account;\n }\n\n if ($account === null) {\n return [];\n }\n\n $params = [\n 'lead_id' => $account->crm_provider_id,\n '_order_by' => '-date_updated',\n ];\n\n $onlyOpen = true;\n switch ($this->config->opportunity_assignment_rule) {\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:\n $params['_order_by'] = '-date_updated';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:\n $params['_order_by'] = '-date_created';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:\n $params['_order_by'] = 'date_created';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:\n $params['_order_by'] = '-date_updated';\n $onlyOpen = false;\n }\n\n if ($onlyOpen) {\n $params['status_type__in'] = 'active,won';\n }\n\n $clOpportunities = $this->client->get('opportunity', $params);\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n foreach ($clOpportunities['data'] as $clOpportunity) {\n $stage = $this->config\n ->stages()\n ->where('crm_provider_id', $clOpportunity['status_id'])\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $clOpportunity['status_id']);\n }\n\n $record = [\n 'crmId' => $clOpportunity['id'],\n 'name' => $clOpportunity['note'],\n 'value' => currency_format($clOpportunity['value'], $clOpportunity['value_currency']),\n 'won' => $stage->probability === 100.00,\n 'closed' => $clOpportunity['status_type'] !== 'active',\n 'stage' => [\n 'id' => $stage->id_string,\n 'name' => $stage->name,\n ],\n 'recordType' => [],\n ];\n\n if ($ownerId && isset($clOpportunity['user_id']) && $clOpportunity['user_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n $crmId = null;\n\n if ($objectType === 'contact') {\n $contact = $this->syncContact($objectId);\n\n if ($contact && $contact->account_id) {\n $crmId = $contact->account->crm_provider_id;\n }\n } else {\n $crmId = $objectId;\n }\n\n if ($crmId) {\n $clTasks = $this->client->get('task', [\n 'lead_id' => $crmId,\n '_type' => 'lead',\n 'assigned_to' => $this->profile->crm_provider_id,\n 'is_complete' => 'false',\n '_order_by' => 'date',\n ]);\n\n foreach ($clTasks['data'] as $clTask) {\n $data[] = [\n 'crmId' => $clTask['id'],\n 'subject' => $clTask['text'],\n 'due' => $clTask['date'] ?? null,\n 'type' => null,\n ];\n }\n }\n\n return $data;\n }\n\n /**\n * Try to find email address in CRM service\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $clObject = $this->client->get('lead', [\n 'query' => 'sort:updated contact(email(email:\"' . $email . '\"))',\n '_limit' => 1,\n ]);\n\n if ($clObject['total_results'] > 0) {\n foreach ($clObject['data'][0]['contacts'] as $clContact) {\n foreach ($clContact['emails'] as $clEmail) {\n if ($email === $clEmail['email']) {\n $data = $this->convertCrmData($clContact['id'], $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n }\n }\n }\n\n return null;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n // Check if the user is internal.\n $teamMember = $this->team->users()->where('phone', $phone)->exists();\n\n // Skip the attendee if internal.\n if ($teamMember === false) {\n $clObject = $this->client->get('lead', [\n 'query' => 'sort:updated contact(' . $phone . ')',\n '_limit' => 1,\n ]);\n\n if ($clObject['total_results'] > 0) {\n foreach ($clObject['data'][0]['contacts'] as $clContact) {\n foreach ($clContact['phones'] as $clPhone) {\n if ($phone === $clPhone['phone']) {\n $data = $this->convertCrmData($clContact['id'], $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n }\n }\n }\n }\n\n return null;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $clObject = $this->client->get('lead', [\n 'query' => 'sort:updated contact(name:\"' . $name . '\")',\n '_limit' => 1,\n ]);\n\n if ($clObject['total_results'] > 0) {\n foreach ($clObject['data'][0]['contacts'] as $clContact) {\n if ($clContact['name'] === $name || $clContact['display_name'] === $name) {\n $data = $this->convertCrmData($clContact['id'], $userId);\n\n return ! empty(array_filter($data)) ? $data : false;\n }\n }\n }\n\n return false;\n });\n\n return is_array($result) ? $result : null;\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n private function convertCrmData(string $crmId, ?int $userId = null): array\n {\n $lead = null;\n $opportunity = null;\n $account = null;\n $stage = null;\n $countryCode = null;\n\n $contact = $this->syncContact($crmId);\n if ($contact) {\n $account = $contact->account;\n\n if ($contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account) {\n $countryCode = $account->country_code;\n }\n\n try {\n $cpOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId,\n );\n\n if (! empty($cpOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($cpOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception) {\n // Nothing to see here.\n }\n }\n\n return [\n $lead,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->type) {\n case Activity::TYPE_CONFERENCE:\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildCallPayload($activity);\n\n break;\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n private function mapStatus(string $status): string\n {\n switch ($status) {\n case Activity::STATUS_COMPLETED:\n case Activity::STATUS_IN_PROGRESS:\n case Activity::STATUS_FAILED:\n case Activity::STATUS_NO_ANSWER:\n case Activity::STATUS_BUSY:\n default:\n return $status;\n case Activity::STATUS_CANCELLED:\n return 'cancel';\n }\n }\n\n /**\n * @throws CrmException\n */\n private function buildCallPayload(Activity $activity): Activity\n {\n try {\n if ($activity->crm_provider_id) {\n // The activity should be logged under the existing Task (not Activity).\n $data = [\n '_type' => 'lead',\n 'lead_id' => $activity->account->crm_provider_id,\n 'assigned_to' => $this->profile->crm_provider_id,\n 'text' => $this->generateActivityDescription($activity),\n 'date' => $activity->getActualEndTime()->toDateString(),\n 'is_complete' => true,\n ];\n\n $this->logger->info('[Close CRM] Updating task', [\n 'activity' => $activity->id,\n 'crm_id' => $activity->crm_provider_id,\n 'data' => $data,\n ]);\n\n $this->client->put('task/' . $activity->crm_provider_id, $data);\n } else {\n // Just create an activity.\n $data = [\n 'lead_id' => $activity->account_id ? $activity->account->crm_provider_id : null,\n 'contact_id' => $activity->contact_id ? $activity->contact->crm_provider_id : null,\n 'created_by' => $this->profile->crm_provider_id,\n 'user_id' => $this->profile->crm_provider_id,\n 'direction' => $activity->getCrmType() === Activity::TYPE_SOFTPHONE ? 'outbound' : 'inbound',\n 'status' => $this->mapStatus($activity->getStatus()),\n 'note' => $this->generateActivityDescription($activity),\n 'duration' => $this->mapStatus($activity->getStatus()) !== 'cancel' ? $activity->duration : 0,\n 'phone' => $activity->to ? $activity->to->phone_number : null,\n ];\n\n $clActivity = $this->client->post('activity/call', $data);\n\n $this->logger->info('[Close CRM] Creating activity', [\n 'activity' => $activity->id,\n 'crm_id' => $clActivity['id'],\n 'data' => $data,\n 'response' => $clActivity,\n ]);\n\n $activity->crm_provider_id = $clActivity['id'];\n $activity->save();\n }\n } catch (ClientException $exception) {\n $response = $exception->getResponse();\n\n if ($response === null) {\n // Trying to debug weird cases where this is null.\n Sentry::captureException($exception);\n }\n\n $responseBody = $response->getBody();\n $message = $responseBody;\n $errorCode = $response->getStatusCode();\n\n $jsonResponse = json_decode($responseBody, true);\n if (isset($jsonResponse[0]['message'])) {\n $message = $jsonResponse[0]['message'];\n }\n\n throw new CrmException($message, $errorCode);\n }\n\n return $activity;\n }\n\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $clActivity = $this->client->post('activity/sms', [\n 'lead_id' => $activity->account_id ? $activity->account->crm_provider_id : null,\n 'contact_id' => $activity->contact_id ? $activity->contact->crm_provider_id : null,\n 'created_by' => $this->profile->crm_provider_id,\n 'user_id' => $this->profile->crm_provider_id,\n 'status' => $activity->getType() === Activity::TYPE_SMS_OUTBOUND ? 'sent' : 'inbox',\n 'text' => $this->generateActivityDescription($activity),\n 'remote_phone' => $activity->to ? $activity->to->phone_number : null,\n 'local_phone' => $activity->to ? $activity->to->phone_number : null,\n 'source' => 'Close.io',\n ]);\n\n $activity->crm_provider_id = $clActivity['id'];\n $activity->save();\n\n return $activity;\n }\n\n private function generateActivityDescription(Activity $activity): string\n {\n $description = '';\n\n switch ($activity->type) {\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n case Activity::TYPE_CONFERENCE:\n if ($activity->hasActivityType()) {\n $description = $activity->getActivityType()->getName() . PHP_EOL . PHP_EOL;\n }\n if ($activity->hasTitle()) {\n $description .= $activity->getTitle() . PHP_EOL;\n }\n\n if ($activity->hasReasonCodeBotKicked()) {\n $description .= 'Notetaker removed from this meeting' . PHP_EOL . PHP_EOL;\n // When we fix the state to be Activity::RECORDING_RECORDED as it should be this can change.\n } elseif ($activity->hasReasonCodeNotCompliant()) {\n $description .= 'Notetaker did not join due to recording consent not being provided by attendees' . PHP_EOL . PHP_EOL;\n } elseif ($activity->canReviewActivity()) {\n $playbackUrl = $activity->user->team->partner->getPlaybackUrl($activity);\n $description .= 'Review Activity:' . PHP_EOL . $playbackUrl . PHP_EOL . PHP_EOL;\n }\n\n if ($activity->type === Activity::TYPE_CONFERENCE) {\n $description .= 'Attendees:'\n . PHP_EOL\n . (new FilterJoinedParticipants())->toString($activity);\n }\n\n if (\\count($activity->notes) > 0) {\n $description .= PHP_EOL . PHP_EOL . 'Notes: ' . PHP_EOL;\n\n foreach ($activity->notes as $note) {\n $time = ($note->time > 3600) ? gmdate('H:i:s', $note->time) : gmdate('i:s', $note->time);\n $description .= $time . ' ' . $note->note . PHP_EOL;\n }\n }\n\n // Get all private messages.\n $messages = $activity->messages()\n ->where('is_private', 1)\n ->orderBy('created_at', 'asc');\n\n if ($messages->count() > 0) {\n $description .= PHP_EOL . PHP_EOL . 'Coaching Chat: ' . PHP_EOL;\n\n foreach ($messages->get() as $message) {\n $description .= $message->participant->name . ': ' . $message->message . PHP_EOL;\n }\n }\n\n // Get all public messages.\n $messages = $activity->messages()\n ->where('is_private', 0)\n ->orderBy('created_at', 'asc');\n\n if ($messages->count() > 0) {\n $description .= PHP_EOL . PHP_EOL . 'Customer Chat: ' . PHP_EOL;\n\n foreach ($messages->get() as $message) {\n $description .= $message->participant->name . ': ' . $message->message . PHP_EOL;\n }\n }\n\n if ($activity->summary) {\n $description .= PHP_EOL . PHP_EOL . 'Summary: ' . PHP_EOL . $activity->summary;\n }\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $description = $activity->description;\n\n break;\n }\n\n return $description;\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n // This is the user provided activity subject field.\n if (empty($fields['name'])) {\n return null;\n }\n\n $due = null;\n if (empty($fields['due_date']) === false) {\n $formatDue = Carbon::parse($fields['due_date']);\n $due = $formatDue->toDateTimeString();\n }\n\n $clTask = $this->client->post('task', [\n '_type' => 'lead',\n 'lead_id' => $activity->account->crm_provider_id,\n 'assigned_to' => $this->profile->crm_provider_id,\n 'text' => $fields['name'],\n 'date' => $due,\n 'is_complete' => false,\n ]);\n\n // We don't actually create a corresponding activity object on our side yet.\n return $clTask['id'];\n }\n\n /**\n * Store transcripts as note.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n if ($activity->account_id === null) {\n // We can only log to accounts (leads).\n return;\n }\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, self::NOTE_BODY_MAX_LENGTH);\n\n $clActivity = $this->client->post('activity/note', [\n 'lead_id' => $activity->account_id ? $activity->account->crm_provider_id : null,\n 'created_by' => $this->profile->crm_provider_id,\n 'user_id' => $this->profile->crm_provider_id,\n 'note' => $transcripts,\n ]);\n\n // Store CRM Activity ID in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $clActivity['id'];\n $transcription->save();\n }\n\n public function parseObjectType(string $objectId): string\n {\n if (Str::startsWith($objectId, 'lead')) {\n return 'account';\n }\n\n if (Str::startsWith($objectId, 'cont')) {\n return 'contact';\n }\n\n if (Str::startsWith($objectId, 'oppo')) {\n return 'opportunity';\n }\n\n throw new InvalidArgumentException('Unsupported Object Type');\n }\n\n /**\n * @inheritdoc\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n if ($crmObject instanceof Lead) {\n // This would never get invoked since we merge lead/accounts in Close.\n $this->client->put('lead/' . $crmObject->crm_provider_id, [\n 'status' => $stage->crm_provider_id,\n ]);\n } else {\n $this->client->put('opportunity/' . $crmObject->crm_provider_id, [\n 'status_id' => $stage->crm_provider_id,\n ]);\n }\n }\n\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $this->getClient()->updateObject($this->convertObjectTypeToResource($objectType), $objectId, $data);\n }\n\n public function prepareValueForUpdate(array $params): array\n {\n $convertedValue = $this->fieldValueConverter->convertToCrm(\n $this->config,\n $params['fieldName'],\n $params['fieldValue'],\n );\n\n if ($this->isCustomField($params['fieldName'])) {\n $params['fieldName'] = 'custom.' . $params['fieldName'];\n }\n\n $params['fieldValue'] = $convertedValue;\n\n return parent::prepareValueForUpdate($params);\n }\n\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n return $this->client->get($this->convertObjectTypeToResource($objectType) . '/' . $objectId);\n }\n\n /**\n *\n * @throws UnexpectedValueException\n */\n private function convertObjectTypeToResource(string $objectType): string\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return 'opportunity';\n\n case FieldData::OBJECT_CONTACT:\n return 'contact';\n\n case FieldData::OBJECT_ACCOUNT:\n return 'lead';\n\n case FieldData::OBJECT_TASK:\n return 'activity';\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $baseUrl = 'https://app.close.com/';\n $url = null;\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'lead/' . $providerId;\n\n break;\n\n case 'contact':\n $contact = $this->config->contacts()->where('crm_provider_id', $providerId)->first();\n if ($contact && $contact->account_id) {\n $url = $baseUrl . 'lead/' . $contact->account->crm_provider_id;\n }\n\n break;\n\n default:\n // Sadly we can't deeplink to anything else in Close UI.\n $url = null;\n }\n\n return $url;\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n return $this->transcriptionService\n ->findTranscriptionByActivity($activity)\n ->map(static function (array $transcriptionSegment): string {\n return $transcriptionSegment['formattedStartsAt'] . ' | ' . $transcriptionSegment['transcript'];\n })\n ->implode(PHP_EOL);\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $client = $this->getClient();\n $task = $client->get('task/' . $crmProviderId);\n\n return ! empty($task);\n } catch (HttpNotFoundException) {\n // Task not found in CRM - this is expected and permanent\n $this->logger->info('[Close] Task not found during verification', [\n 'task_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n } catch (CloseException $e) {\n // Handle 404 responses from Close API\n if ($e->getResponseStatusCode() === 404) {\n $this->logger->info('[Close] Task not found during verification (404)', [\n 'task_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n\n // Re-throw other Close exceptions for retry\n throw $e;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"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.7124335,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-8408297265552095730
|
-9030663327281178587
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
6
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Close;
use Cache;
use Carbon\Carbon;
use Exception;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\CloseInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\ServiceUnavailableException;
use Jiminny\Exceptions\UnexpectedCallException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Close\Processor\AccountProcessor;
use Jiminny\Services\Crm\Close\Processor\MetadataProcessor;
use Jiminny\Services\Crm\Close\Processor\OpportunityProcessor;
use Jiminny\Services\Crm\Close\Processor\StageProcessor;
use Jiminny\Services\Crm\Helpers\FilterJoinedParticipants;
use Jiminny\Services\Crm\Metadata\OpportunityMetadata;
use Jiminny\Services\Crm\Metadata\ProfileMetadata;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Sentry;
use UnexpectedValueException;
class Service extends BaseService implements
CloseInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
RemoteEntityManipulationInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
SupportsObjectTypeParseInterface,
VerifyTaskExistsInterface
{
private const int NOTE_BODY_MAX_LENGTH = 3000000;
private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day
private StandardFieldMetadata $standardFieldMetadata;
private MetadataProcessor $metadataProcessor;
private FieldValueConverter $fieldValueConverter;
private StageProcessor $stageProcessor;
private OpportunityProcessor $opportunityProcessor;
private AccountProcessor $accountProcessor;
public function __construct(
Client $client,
StandardFieldMetadata $standardFieldMetadata,
MetadataProcessor $metadataProcessor,
FieldValueConverter $fieldValueConverter,
StageProcessor $stageResolver,
OpportunityProcessor $opportunityProcessor,
AccountProcessor $accountProcessor,
private readonly ProspectPhotoPathService $prospectPhotoPathService,
) {
parent::__construct();
$this->client = $client;
$this->standardFieldMetadata = $standardFieldMetadata;
$this->metadataProcessor = $metadataProcessor;
$this->fieldValueConverter = $fieldValueConverter;
$this->stageProcessor = $stageResolver;
$this->opportunityProcessor = $opportunityProcessor;
$this->accountProcessor = $accountProcessor;
}
public function getDisplayName(): string
{
return 'Close';
}
public function setConfiguration(Configuration $config): void
{
parent::setConfiguration($config);
$this->metadataProcessor->setConfiguration($config);
$this->stageProcessor->setConfiguration($config);
$this->opportunityProcessor->setConfiguration($config);
$this->accountProcessor->setConfiguration($config);
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
return $user->getTeam()->getOwner()->getSocialAccount(SocialAccount::PROVIDER_CLOSE);
}
private function getClient(): Client
{
if (! $this->client instanceof Client) {
throw new UnexpectedCallException('Client not set');
}
return $this->client;
}
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return $this->fieldValueConverter->convertFromCrm($fieldType, $fieldValue);
}
protected function getFieldTypes(): array
{
return [
parent::OBJECT_OPPORTUNITY,
parent::OBJECT_CONTACT,
parent::OBJECT_ACCOUNT,
];
}
protected function getFields(string $crmObject): array
{
// not used
return [];
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
// Set up the activity field as the default Type.
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'type',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
public function syncFields(): void
{
$this->syncStandardFields();
$this->syncCustomFields();
}
/**
* @important Works only for custom fields
*/
public function syncField(Field $field): void
{
$resource = $this->convertObjectTypeToResource($field->getObjectType());
// We can only sync custom fields in this CRM.
if ($this->isCustomField($field->getCrmProviderId()) === false) {
return;
}
$crmField = $this->getClient()->fetchCustomFieldDefinition($resource, $field->getCrmProviderId());
$this->metadataProcessor->syncField($crmField);
}
private function isCustomField(string $fieldId): bool
{
return strpos($fieldId, 'cf_') === 0;
}
/**
* @inheritdoc
*/
public function importPicklistValues(Field $field): array
{
// handled in syncFields()
return [];
}
/**
* @important We only support stages on the opportunity object
*
* @param string[]|null $types
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
if (! $missingStageName) {
// This is taken care of by syncOrganization()
return null;
}
$stage = $this->stageProcessor->resolveFromStageId($missingStageName);
if ($stage instanceof Stage) {
return $stage;
}
$stageMetadata = $this->getClient()->fetchStage($missingStageName);
if (! $stageMetadata) {
$this->logger->error('Stage does not exist', [
'stage' => $missingStageName,
]);
return null;
}
return $this->stageProcessor->importStage($stageMetadata);
}
/**
* @inheritdoc
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Even though Close.io has the concept of "leads", they fit more into our concept of accounts.
return 0;
}
/**
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Not a supported entity.
return null;
}
/**
* @throws Exception
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
foreach ($this->getClient()->listAccounts($since) as $clAccount) {
// Only sync if previously imported.
if ($this->hasAccount($clAccount->getId())) {
$this->importAccount($clAccount);
$syncCount++;
}
}
} catch (Exception $exception) {
$this->logger->error('Account sync failed', [
'error' => $exception->getMessage(),
]);
throw $exception;
}
return $syncCount;
}
public function syncAccount(string $crmId): ?Account
{
return $this->accountProcessor->syncAccount($crmId);
}
private function importAccount($crmData): Account
{
return $this->accountProcessor->importAccountMetadata($crmData);
}
/**
* @throws CloseException
*/
public function syncOpportunities(array $parameters, ?string $strategy = null): int
{
$strategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$strategies = $strategyResolver->getStrategies($this->config, $strategy);
$syncCount = 0;
try {
$opportunities = [];
foreach ($strategies as $syncStrategy) {
$opportunitiesData = $syncStrategy->fetchOpportunities($parameters);
$opportunities[] = $opportunitiesData['data'];
if ($opportunitiesData['has_more']) {
$this->logger->info('[Close] Sync Opportunities - count warning', [
'team_id' => $this->config->getTeam()->getId(),
'total' => $opportunitiesData['total'],
'count' => $opportunitiesData['count'],
'skip' => $opportunitiesData['skip'],
'strategies_count' => count($strategies),
]);
}
}
$opportunities = array_merge(...$opportunities);
} catch (CrmException $exception) {
$this->logger->error('Fetching opportunity data failed', [
'team' => $this->getTeam()->getSlug(),
'error' => $exception->getMessage(),
]);
return 0;
}
foreach ($opportunities as $opportunityMetadata) {
try {
$this->importOpportunity($opportunityMetadata);
$syncCount++;
} catch (Exception $exception) {
$this->logger->warning('Opportunity sync failed', [
'opportunity' => $opportunityMetadata->getId(),
'error' => $exception->getMessage(),
]);
}
}
return $syncCount;
}
public function syncOpportunity(string $crmId): ?Opportunity
{
$strategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$strategy = $strategyResolver->resolve(
$this->config,
OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY,
);
$parameters = ['crm_id' => $crmId];
$opportunity = $strategy->fetchOpportunities($parameters);
if (empty($opportunity['data'])) {
return null;
}
return $this->importOpportunity($opportunity['data']);
}
private function importOpportunity(OpportunityMetadata $crmData): ?Opportunity
{
if (! $crmData->getLeadId()) {
$this->logger->warning('Opportunity does not have a lead ID', [
'opportunity' => $crmData->getId(),
]);
return null;
}
$account = $this->getConfiguration()
->accounts()
->where('crm_provider_id', $crmData->getLeadId())
->first();
if ($account === null) {
$account = $this->accountProcessor->syncAccount($crmData->getLeadId());
}
/** @var Profile $profile */
$profile = $this->getConfiguration()
->profiles()
->where('crm_provider_id', $crmData->getUserId())
->first();
$userId = $profile?->getUserId() ?? $account?->getUserId();
if ($userId === null) {
$this->logger->error('[Close] | Skip import, no user_id found', [
'id' => $crmData->getId(),
]);
return null;
}
$stage = $this->getConfiguration()
->stages()
->where('crm_provider_id', $crmData->getStageId())
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $crmData->getStageId());
}
return $this->opportunityProcessor->import($crmData, $account, $stage, $profile);
}
/**
* @param array<string,string> $crmData
* @param string[] $crmFields
*/
public function importOpportunityCrmFieldData(array $crmData, array $crmFields, int $opportunityId): void
{
// handled in importOpportunity
}
/**
* @inheritdoc
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
/** No way to sync today.
$clContacts = $this->client->get('lead', [
'date_updated__gte' => $since->toDateString(),
'_order_by' => '-date_updated',
]);
foreach ($clContacts as $clContact) {
// Only sync if previously imported.
if ($this->hasContact($clContact['id'])) {
$this->importContact($clContact);
$syncCount++;
}
}
**/
} catch (Exception $exception) {
// Do nothing for now.
throw $exception;
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
$clContact = $this->client->get('contact/' . $crmId);
} catch (HttpNotFoundException $exception) {
return null;
}
return $this->importContact($clContact);
}
/**
* @inheritdoc
*/
private function importContact($crmData): Contact
{
$account = null;
if ($crmData['lead_id']) {
$account = $this->team
->accounts()
->where('crm_provider_id', $crmData['lead_id'])
->first();
if ($account === null) {
$account = $this->syncAccount($crmData['lead_id']);
}
}
$mobilePhone = $parsedNumber = null;
foreach ($crmData['phones'] as $phoneNumber) {
if ($phoneNumber['type'] === 'mobile') {
$mobilePhone = $this->buildContactMobilePhone(null, $phoneNumber['phone']);
} else {
$parsedNumber = $this->buildContactPhone(null, $phoneNumber['phone']);
}
}
$email = null;
if (empty($crmData['emails']) === false) {
$email = mb_strimwidth($crmData['emails'][0]['email'], 0, 80);
}
$profile = $this->config->profiles()->where('crm_provider_id', (string) $crmData['updated_by'])->first();
$data = [
'account_id' => $account->id ?? null,
'team_id' => $this->team->id,
'user_id' => $profile?->user_id,
'owner_id' => $crmData['updated_by'],
'name' => $crmData['name'] ?? 'Unknown',
'email' => $email,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobilePhone ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'title' => mb_strimwidth($crmData['title'] ?? '', 0, 128),
'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(
crmConfiguration: $this->config,
crmProviderId: $crmData['id'],
modelType: Contact::class,
fileName: $crmData['id'],
avatarText: $crmData['name'] ?? 'Unknown'
),
'remotely_created_at' => Carbon::parse($crmData['date_created']),
];
/** @var Contact */
return $this->config->contacts()->updateOrCreate(['crm_provider_id' => (string) $crmData['id']], $data);
}
private function buildContactPhone(?string $countryCode, ?string $number): ?array
{
if ($number) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($number, 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
return $parsedNumber;
}
private function buildContactMobilePhone(?string $countryCode, ?string $number): ?string
{
return $number ? mb_strimwidth(phone_e164($countryCode, $number), 0, 25) : null;
}
public function syncOrganization(): void
{
$organisation = $this->getClient()->fetchOrganisation();
$this->metadataProcessor->syncOrganisation($organisation);
foreach ($organisation->getPipelines() as $pipelineMetadata) {
$this->metadataProcessor->syncPipeline($pipelineMetadata);
}
}
private function syncStandardFields(): void
{
// Currently we sync only opportunity fields
$stages = $this->getClient()->listStages();
foreach ($this->standardFieldMetadata->opportunity($stages) as $fieldMetadata) {
$this->metadataProcessor->syncField($fieldMetadata);
}
$this->config->save();
}
private function syncCustomFields(): void
{
foreach ($this->getFieldTypes() as $fieldType) {
$objectType = $this->convertObjectTypeToResource($fieldType);
$currentFields = $this->getClient()->listCustomFieldDefinitions($objectType);
foreach ($currentFields as $fieldMetadata) {
$this->metadataProcessor->syncField($fieldMetadata);
}
}
$this->config->save();
}
public function syncProfiles(?User $userToSearch = null): ?Profile
{
/*
* Fetch the profile of the user from the database
* Then fetch the user metadata from Close and update it
* In case there's no profile for the user, proceed with syncing all users
*/
$foundUser = null;
if ($userToSearch) {
$profile = $userToSearch->getProfile();
if ($profile instanceof Profile) {
$crmProviderId = $profile->getCrmProviderId();
if ($crmProviderId) {
$profileMetadata = $this->getClient()->fetchUser($crmProviderId);
if (! $profileMetadata instanceof ProfileMetadata) {
return null;
}
return $this->metadataProcessor->syncProfile($profileMetadata);
}
}
}
foreach ($this->getClient()->listUsers() as $userMetadata) {
$userProfile = $this->metadataProcessor->syncProfile($userMetadata);
if (
$userToSearch instanceof User
&& $userProfile instanceof Profile
&& $userProfile->getUserId() === $userToSearch->getId()
) {
$foundUser = $userProfile;
}
}
return $foundUser;
}
public function syncProfileFields(): void
{
// Not used.
}
/**
* @inheritdoc
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
$data = Cache::remember($this->team->id . $name . $count . $offset, 300, function () use ($name, $count, $offset) {
$data = [];
try {
// If search phrase resembles phone number remove special symbols
if (preg_match('/^([0-9\s\-\+\(\)]*)$/', $name)) {
$name = '+' . preg_replace('/[\s\-\+\(\)]/', '', $name);
}
// Close do not provide a unified way to search, so we must hack our own.
$objects = $this->client->get('lead', [
'query' => 'sort:date_updated name:(' . $name . ') or email:(' . $name . ') or phone:(' . $name . ')',
'_limit' => $count, '_skip' => $offset,
]);
} catch (\GuzzleHttp\Exception\ServerException $exception) {
throw new ServiceUnavailableException($exception->getMessage());
}
foreach ($objects['data'] as $object) {
// We need a contact to dial it.
if (empty($object['contacts'])) {
continue;
}
foreach ($object['contacts'] as $contact) {
$record = [
'crmId' => $contact['id'],
'crmUrl' => $this->generateProviderUrl($object['id'], 'account'),
'name' => $contact['name'],
'industry' => null,
'title' => $contact['title'],
'organization' => $object['display_name'],
'prospectType' => 'contact',
'phoneNumbers' => [],
];
foreach ($contact['phones'] as $phone) {
if ($phone['type'] === 'mobile') {
$number = $this->buildContactMobilePhone(null, $phone['phone']);
$record['phoneNumbers'][] = [
'number' => $number,
'nationalFormat' => phone_national(null, $number),
'type' => 'mobile',
];
} else {
$parsedNumber = $this->buildContactPhone(null, $phone['phone']);
// Add phone number to record.
if (empty($parsedNumber['phone']) === false) {
$record['phoneNumbers'][] = [
'number' => $parsedNumber['phone'],
'nationalFormat' => phone_national(null, $parsedNumber['phone']),
'type' => 'phone',
];
}
}
}
$data[] = $record;
}
}
return $data;
});
return $data;
}
/**
* @inheritdoc
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
$contact = null;
$account = null;
if ($crmAccountId) {
$account = $this->config->accounts()->where('crm_provider_id', $crmAccountId)->first();
if ($account === null) {
$account = $this->syncAccount($crmAccountId);
}
}
if ($crmContactId) {
$contact = $this->config->contacts()->where('crm_provider_id', $crmContactId)->first();
if ($contact === null) {
$contact = $this->syncContact($crmContactId);
}
}
if ($contact || $account) {
if ($contact && $account === null) {
$account = $contact->account;
}
if ($account === null) {
return [];
}
$params = [
'lead_id' => $account->crm_provider_id,
'_order_by' => '-date_updated',
];
$onlyOpen = true;
switch ($this->config->opportunity_assignment_rule) {
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:
$params['_order_by'] = '-date_updated';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:
$params['_order_by'] = '-date_created';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:
$params['_order_by'] = 'date_created';
break;
case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:
$params['_order_by'] = '-date_updated';
$onlyOpen = false;
}
if ($onlyOpen) {
$params['status_type__in'] = 'active,won';
}
$clOpportunities = $this->client->get('opportunity', $params);
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
foreach ($clOpportunities['data'] as $clOpportunity) {
$stage = $this->config
->stages()
->where('crm_provider_id', $clOpportunity['status_id'])
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $clOpportunity['status_id']);
}
$record = [
'crmId' => $clOpportunity['id'],
'name' => $clOpportunity['note'],
'value' => currency_format($clOpportunity['value'], $clOpportunity['value_currency']),
'won' => $stage->probability === 100.00,
'closed' => $clOpportunity['status_type'] !== 'active',
'stage' => [
'id' => $stage->id_string,
'name' => $stage->name,
],
'recordType' => [],
];
if ($ownerId && isset($clOpportunity['user_id']) && $clOpportunity['user_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
$crmId = null;
if ($objectType === 'contact') {
$contact = $this->syncContact($objectId);
if ($contact && $contact->account_id) {
$crmId = $contact->account->crm_provider_id;
}
} else {
$crmId = $objectId;
}
if ($crmId) {
$clTasks = $this->client->get('task', [
'lead_id' => $crmId,
'_type' => 'lead',
'assigned_to' => $this->profile->crm_provider_id,
'is_complete' => 'false',
'_order_by' => 'date',
]);
foreach ($clTasks['data'] as $clTask) {
$data[] = [
'crmId' => $clTask['id'],
'subject' => $clTask['text'],
'due' => $clTask['date'] ?? null,
'type' => null,
];
}
}
return $data;
}
/**
* Try to find email address in CRM service
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$clObject = $this->client->get('lead', [
'query' => 'sort:updated contact(email(email:"' . $email . '"))',
'_limit' => 1,
]);
if ($clObject['total_results'] > 0) {
foreach ($clObject['data'][0]['contacts'] as $clContact) {
foreach ($clContact['emails'] as $clEmail) {
if ($email === $clEmail['email']) {
$data = $this->convertCrmData($clContact['id'], $userId);
return ! empty(array_filter($data)) ? $data : null;
}
}
}
}
return null;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
// Check if the user is internal.
$teamMember = $this->team->users()->where('phone', $phone)->exists();
// Skip the attendee if internal.
if ($teamMember === false) {
$clObject = $this->client->get('lead', [
'query' => 'sort:updated contact(' . $phone . ')',
'_limit' => 1,
]);
if ($clObject['total_results'] > 0) {
foreach ($clObject['data'][0]['contacts'] as $clContact) {
foreach ($clContact['phones'] as $clPhone) {
if ($phone === $clPhone['phone']) {
$data = $this->convertCrmData($clContact['id'], $userId);
return ! empty(array_filter($data)) ? $data : null;
}
}
}
}
}
return null;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$clObject = $this->client->get('lead', [
'query' => 'sort:updated contact(name:"' . $name . '")',
'_limit' => 1,
]);
if ($clObject['total_results'] > 0) {
foreach ($clObject['data'][0]['contacts'] as $clContact) {
if ($clContact['name'] === $name || $clContact['display_name'] === $name) {
$data = $this->convertCrmData($clContact['id'], $userId);
return ! empty(array_filter($data)) ? $data : false;
}
}
}
return false;
});
return is_array($result) ? $result : null;
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
private function convertCrmData(string $crmId, ?int $userId = null): array
{
$lead = null;
$opportunity = null;
$account = null;
$stage = null;
$countryCode = null;
$contact = $this->syncContact($crmId);
if ($contact) {
$account = $contact->account;
if ($contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account) {
$countryCode = $account->country_code;
}
try {
$cpOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId,
);
if (! empty($cpOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($cpOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception) {
// Nothing to see here.
}
}
return [
$lead,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
public function saveActivity(Activity $activity): Activity
{
switch ($activity->type) {
case Activity::TYPE_CONFERENCE:
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildCallPayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
private function mapStatus(string $status): string
{
switch ($status) {
case Activity::STATUS_COMPLETED:
case Activity::STATUS_IN_PROGRESS:
case Activity::STATUS_FAILED:
case Activity::STATUS_NO_ANSWER:
case Activity::STATUS_BUSY:
default:
return $status;
case Activity::STATUS_CANCELLED:
return 'cancel';
}
}
/**
* @throws CrmException
*/
private function buildCallPayload(Activity $activity): Activity
{
try {
if ($activity->crm_provider_id) {
// The activity should be logged under the existing Task (not Activity).
$data = [
'_type' => 'lead',
'lead_id' => $activity->account->crm_provider_id,
'assigned_to' => $this->profile->crm_provider_id,
'text' => $this->generateActivityDescription($activity),
'date' => $activity->getActualEndTime()->toDateString(),
'is_complete' => true,
];
$this->logger->info('[Close CRM] Updating task', [
'activity' => $activity->id,
'crm_id' => $activity->crm_provider_id,
'data' => $data,
]);
$this->client->put('task/' . $activity->crm_provider_id, $data);
} else {
// Just create an activity.
$data = [
'lead_id' => $activity->account_id ? $activity->account->crm_provider_id : null,
'contact_id' => $activity->contact_id ? $activity->contact->crm_provider_id : null,
'created_by' => $this->profile->crm_provider_id,
'user_id' => $this->profile->crm_provider_id,
'direction' => $activity->getCrmType() === Activity::TYPE_SOFTPHONE ? 'outbound' : 'inbound',
'status' => $this->mapStatus($activity->getStatus()),
'note' => $this->generateActivityDescription($activity),
'duration' => $this->mapStatus($activity->getStatus()) !== 'cancel' ? $activity->duration : 0,
'phone' => $activity->to ? $activity->to->phone_number : null,
];
$clActivity = $this->client->post('activity/call', $data);
$this->logger->info('[Close CRM] Creating activity', [
'activity' => $activity->id,
'crm_id' => $clActivity['id'],
'data' => $data,
'response' => $clActivity,
]);
$activity->crm_provider_id = $clActivity['id'];
$activity->save();
}
} catch (ClientException $exception) {
$response = $exception->getResponse();
if ($response === null) {
// Trying to debug weird cases where this is null.
Sentry::captureException($exception);
}
$responseBody = $response->getBody();
$message = $responseBody;
$errorCode = $response->getStatusCode();
$jsonResponse = json_decode($responseBody, true);
if (isset($jsonResponse[0]['message'])) {
$message = $jsonResponse[0]['message'];
}
throw new CrmException($message, $errorCode);
}
return $activity;
}
private function buildTextMessagePayload(Activity $activity): Activity
{
$clActivity = $this->client->post('activity/sms', [
'lead_id' => $activity->account_id ? $activity->account->crm_provider_id : null,
'contact_id' => $activity->contact_id ? $activity->contact->crm_provider_id : null,
'created_by' => $this->profile->crm_provider_id,
'user_id' => $this->profile->crm_provider_id,
'status' => $activity->getType() === Activity::TYPE_SMS_OUTBOUND ? 'sent' : 'inbox',
'text' => $this->generateActivityDescription($activity),
'remote_phone' => $activity->to ? $activity->to->phone_number : null,
'local_phone' => $activity->to ? $activity->to->phone_number : null,
'source' => 'Close.io',
]);
$activity->crm_provider_id = $clActivity['id'];
$activity->save();
return $activity;
}
private function generateActivityDescription(Activity $activity): string
{
$description = '';
switch ($activity->type) {
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
case Activity::TYPE_CONFERENCE:
if ($activity->hasActivityType()) {
$description = $activity->getActivityType()->getName() . PHP_EOL . PHP_EOL;
}
if ($activity->hasTitle()) {
$description .= $activity->getTitle() . PHP_EOL;
}
if ($activity->hasReasonCodeBotKicked()) {
$description .= 'Notetaker removed from this meeting' . PHP_EOL . PHP_EOL;
// When we fix the state to be Activity::RECORDING_RECORDED as it should be this can change.
} elseif ($activity->hasReasonCodeNotCompliant()) {
$description .= 'Notetaker did not join due to recording consent not being provided by attendees' . PHP_EOL . PHP_EOL;
} elseif ($activity->canReviewActivity()) {
$playbackUrl = $activity->user->team->partner->getPlaybackUrl($activity);
$description .= 'Review Activity:' . PHP_EOL . $playbackUrl . PHP_EOL . PHP_EOL;
}
if ($activity->type === Activity::TYPE_CONFERENCE) {
$description .= 'Attendees:'
. PHP_EOL
. (new FilterJoinedParticipants())->toString($activity);
}
if (\count($activity->notes) > 0) {
$description .= PHP_EOL . PHP_EOL . 'Notes: ' . PHP_EOL;
foreach ($activity->notes as $note) {
$time = ($note->time > 3600) ? gmdate('H:i:s', $note->time) : gmdate('i:s', $note->time);
$description .= $time . ' ' . $note->note . PHP_EOL;
}
}
// Get all private messages.
$messages = $activity->messages()
->where('is_private', 1)
->orderBy('created_at', 'asc');
if ($messages->count() > 0) {
$description .= PHP_EOL . PHP_EOL . 'Coaching Chat: ' . PHP_EOL;
foreach ($messages->get() as $message) {
$description .= $message->participant->name . ': ' . $message->message . PHP_EOL;
}
}
// Get all public messages.
$messages = $activity->messages()
->where('is_private', 0)
->orderBy('created_at', 'asc');
if ($messages->count() > 0) {
$description .= PHP_EOL . PHP_EOL . 'Customer Chat: ' . PHP_EOL;
foreach ($messages->get() as $message) {
$description .= $message->participant->name . ': ' . $message->message . PHP_EOL;
}
}
if ($activity->summary) {
$description .= PHP_EOL . PHP_EOL . 'Summary: ' . PHP_EOL . $activity->summary;
}
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$description = $activity->description;
break;
}
return $description;
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
// This is the user provided activity subject field.
if (empty($fields['name'])) {
return null;
}
$due = null;
if (empty($fields['due_date']) === false) {
$formatDue = Carbon::parse($fields['due_date']);
$due = $formatDue->toDateTimeString();
}
$clTask = $this->client->post('task', [
'_type' => 'lead',
'lead_id' => $activity->account->crm_provider_id,
'assigned_to' => $this->profile->crm_provider_id,
'text' => $fields['name'],
'date' => $due,
'is_complete' => false,
]);
// We don't actually create a corresponding activity object on our side yet.
return $clTask['id'];
}
/**
* Store transcripts as note.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
if ($activity->account_id === null) {
// We can only log to accounts (leads).
return;
}
// Generate activity transcription.
$transcriptionData = $this->generateTranscription($activity);
// Truncate Notes with max notes length because transcription text could be very long.
$transcripts = mb_strimwidth($transcriptionData, 0, self::NOTE_BODY_MAX_LENGTH);
$clActivity = $this->client->post('activity/note', [
'lead_id' => $activity->account_id ? $activity->account->crm_provider_id : null,
'created_by' => $this->profile->crm_provider_id,
'user_id' => $this->profile->crm_provider_id,
'note' => $transcripts,
]);
// Store CRM Activity ID in transcription.
$transcription = $activity->getTranscription();
$transcription->crm_activity_id = $clActivity['id'];
$transcription->save();
}
public function parseObjectType(string $objectId): string
{
if (Str::startsWith($objectId, 'lead')) {
return 'account';
}
if (Str::startsWith($objectId, 'cont')) {
return 'contact';
}
if (Str::startsWith($objectId, 'oppo')) {
return 'opportunity';
}
throw new InvalidArgumentException('Unsupported Object Type');
}
/**
* @inheritdoc
*/
public function updateStage($crmObject, Stage $stage): void
{
if ($crmObject instanceof Lead) {
// This would never get invoked since we merge lead/accounts in Close.
$this->client->put('lead/' . $crmObject->crm_provider_id, [
'status' => $stage->crm_provider_id,
]);
} else {
$this->client->put('opportunity/' . $crmObject->crm_provider_id, [
'status_id' => $stage->crm_provider_id,
]);
}
}
public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void
{
$this->getClient()->updateObject($this->convertObjectTypeToResource($objectType), $objectId, $data);
}
public function prepareValueForUpdate(array $params): array
{
$convertedValue = $this->fieldValueConverter->convertToCrm(
$this->config,
$params['fieldName'],
$params['fieldValue'],
);
if ($this->isCustomField($params['fieldName'])) {
$params['fieldName'] = 'custom.' . $params['fieldName'];
}
$params['fieldValue'] = $convertedValue;
return parent::prepareValueForUpdate($params);
}
public function getRecord(string $objectType, string $objectId, array $fields = []): array
{
return $this->client->get($this->convertObjectTypeToResource($objectType) . '/' . $objectId);
}
/**
*
* @throws UnexpectedValueException
*/
private function convertObjectTypeToResource(string $objectType): string
{
switch ($objectType) {
case FieldData::OBJECT_OPPORTUNITY:
return 'opportunity';
case FieldData::OBJECT_CONTACT:
return 'contact';
case FieldData::OBJECT_ACCOUNT:
return 'lead';
case FieldData::OBJECT_TASK:
return 'activity';
default:
throw new UnexpectedValueException('Unsupported object type "' . $objectType . '"');
}
}
public function generateProviderUrl(string $providerId, string $objectType): ?string
{
$baseUrl = 'https://app.close.com/';
$url = null;
switch ($objectType) {
case 'account':
$url = $baseUrl . 'lead/' . $providerId;
break;
case 'contact':
$contact = $this->config->contacts()->where('crm_provider_id', $providerId)->first();
if ($contact && $contact->account_id) {
$url = $baseUrl . 'lead/' . $contact->account->crm_provider_id;
}
break;
default:
// Sadly we can't deeplink to anything else in Close UI.
$url = null;
}
return $url;
}
/**
* Generate transcription for the activity.
*/
private function generateTranscription(Activity $activity): string
{
if (! $this->config->store_transcript) {
// If sending transcription to activity toggle is disabled
return '';
}
return $this->transcriptionService
->findTranscriptionByActivity($activity)
->map(static function (array $transcriptionSegment): string {
return $transcriptionSegment['formattedStartsAt'] . ' | ' . $transcriptionSegment['transcript'];
})
->implode(PHP_EOL);
}
public function verifyTaskExists(Activity $activity): bool
{
$crmProviderId = $activity->getCrmProviderId();
$cacheKey = "crm_task_exists:{$this->config->getId()}:$crmProviderId";
return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {
try {
$client = $this->getClient();
$task = $client->get('task/' . $crmProviderId);
return ! empty($task);
} catch (HttpNotFoundException) {
// Task not found in CRM - this is expected and permanent
$this->logger->info('[Close] Task not found during verification', [
'task_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
...
|
54534
|
NULL
|
NULL
|
NULL
|
|
54536
|
1896
|
59
|
2026-05-18T13:14:29.421464+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110069421_m1.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp(Platform) Refinemen…46 m leftmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com8• Mon 18 May 16:14:28Nikolay Yankov (Presenting)Edit•f•Mon 18 May 16:14*(18@ For you• Recent |# Starred8? Apps07 SpacesSaarco(9 Service-DeskJiminny (Old)Jiminny (New)1 CD Platform TeamIID Capture Team |IID Enterprise Stabllity L..(ID Processing TeamCID SE Kanban= More spaces= Hiters(B Dashboards® Operations83 Confluence?: Teamd% Customise sidebarC Atte© ours* Ciau• Cos: © app | G AlovPipe© 3mLZ EY-Z tY-F Prolhttps://fiminny.atlassian.net/browse/JY-20879- JeeyC Projects|E3 SSH• Home | Salesforceecrоre!Sentry*xIinsights & Coachin…C DevQ Search |+ CreateAsk RovoSpaces Jminty Wven)e, Aod parere/ Д 3-20879• tne message snoua stay onry whse they are adoing tne activity type - & they seave of retresn tne page it snoua asappear, use the into message tnat we naveSubtaskslAooCASALinked work itemsAdd linked work itemActivityCommentsHistoryWork logAdd a comment...Suggest a reply…Who is working on this..?Pro tip: press M to comenemNikolay YankovObectinne"0 Ask Rovo ~y type sync for all CRMs except Zoho, should we do it for the rest, not only SF and Hubspot?• When do we sync - on every change, like type changes for instance? When is the trigger?Estimate:• BE: Maybe 1 day? (Check questions first)• FE: 0.5 day• QA: 1 dayBackiog1 Improve StoryDetailsAssignee& UnassignedAssign to meReporter& Galya DimitrovaQuick start developmentLink this work ltem to your code byincluding keys when creating abranch, commit, or pull requestOismissDeveloomentlQ Open with VS CodeCreate comeComponentsPlatformSub-ProductAdd optionsLabelsNoneStory point estimateNikolay YankovNikolay IvanovAneliya AngelovaGalya DimitrovaLukas Kovalik1:00:564:14 PM | [Platform] Refinement ®Lộ3...
|
NULL
|
7883992976442488310
|
NULL
|
click
|
ocr
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp(Platform) Refinemen…46 m leftmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com8• Mon 18 May 16:14:28Nikolay Yankov (Presenting)Edit•f•Mon 18 May 16:14*(18@ For you• Recent |# Starred8? Apps07 SpacesSaarco(9 Service-DeskJiminny (Old)Jiminny (New)1 CD Platform TeamIID Capture Team |IID Enterprise Stabllity L..(ID Processing TeamCID SE Kanban= More spaces= Hiters(B Dashboards® Operations83 Confluence?: Teamd% Customise sidebarC Atte© ours* Ciau• Cos: © app | G AlovPipe© 3mLZ EY-Z tY-F Prolhttps://fiminny.atlassian.net/browse/JY-20879- JeeyC Projects|E3 SSH• Home | Salesforceecrоre!Sentry*xIinsights & Coachin…C DevQ Search |+ CreateAsk RovoSpaces Jminty Wven)e, Aod parere/ Д 3-20879• tne message snoua stay onry whse they are adoing tne activity type - & they seave of retresn tne page it snoua asappear, use the into message tnat we naveSubtaskslAooCASALinked work itemsAdd linked work itemActivityCommentsHistoryWork logAdd a comment...Suggest a reply…Who is working on this..?Pro tip: press M to comenemNikolay YankovObectinne"0 Ask Rovo ~y type sync for all CRMs except Zoho, should we do it for the rest, not only SF and Hubspot?• When do we sync - on every change, like type changes for instance? When is the trigger?Estimate:• BE: Maybe 1 day? (Check questions first)• FE: 0.5 day• QA: 1 dayBackiog1 Improve StoryDetailsAssignee& UnassignedAssign to meReporter& Galya DimitrovaQuick start developmentLink this work ltem to your code byincluding keys when creating abranch, commit, or pull requestOismissDeveloomentlQ Open with VS CodeCreate comeComponentsPlatformSub-ProductAdd optionsLabelsNoneStory point estimateNikolay YankovNikolay IvanovAneliya AngelovaGalya DimitrovaLukas Kovalik1:00:564:14 PM | [Platform] Refinement ®Lộ3...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
54537
|
1897
|
33
|
2026-05-18T13:14:29.450108+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110069450_m2.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavicarecodeFV faVsco.js°9 master kro PhostormVIewINavicarecodeFV faVsco.js°9 master kroledeym RinaCentralVideo> M SalestorceIm Salesloft> D Talkdeskureams> D Telus>D Twilio_ IWillorlex_ I willorlexDireet• _ I Willo Video_Uploader› _ Vonagewxant1400mZoomBot> ZoomPhoneC) ActivitvcrmFieldsResolver.ohpC) ActivitvLoaService.oho© ActivitvProviderClient.ohp(C) ActivitvProviderRedistrv.oho© ActivityProviderService.php© CallDenormalizerRegistry.phpC) [EMAIL]© DatalmportHandlerInterface.php© MeetingBotService.php© ParticipantConsentService.php© ParticipantsService.phpT ResponseValidationTrait.phpT SalesforceGetUserTrait.php- S/DenormaliserMainCrmDatalrait.onp@ TrackRecordinoFllesizeservice.one© TrackRecordingSizeEnforcer.phpT ValidateEmitProspectEventTrait.phpC AjReports0 AvatarMColondar0 Conference0 Crm> C Bullhornv closeOnoortunitvsvncstrateav• ProcessonProspectSearchStrateav• M TranslatorC) Client.ohr) CloseExcertion.ohoC) FieldDefinitions.onvc) SieldValueConverter ohn•) Service nhnC) StandardFioldMetadata nhn> MConnen© SoftPhoneManager.phpC) CoreUserRequest.pnp© CoreUser.php©Crm/…../Service.php Xclass Service extends BaseService 1mpLementsmA6лv169 C184 0189 0 >194 0213 0222 0>244 6t>* ainherxcdocpublic function getDefaultActivityField(string $activityType): Field{...}* dinherzzdocpublic function getsupportedPlavbooklvpeso: arrav-...public function getbealinsightsFields: arrayl...protected function getbefaultFolLowupLayoutFields(string sactivitylype): array....public function syncFields: voidSthis->svncStandardFieldsoSthis->svncCustomFieldso* @important Works only for custom fieldspublic function syncField(Field $field): void{...}Zusagesprivate function isCustomField(string $fieldId): boolf...}* @inheritdocpublic function importPicklistValues(Field Sfield): array{...}l* oimoortant "e ontu suooort stades on the obdortunitu obtecti* Qoaram strinalllnull Stunesnublic function importStages(2arnav Stvnes = null. ?strina SmissinaStageName = null): 2Stage{...}* Ginhenitdocpublic function syncLeads(Carbon Ssince, ?Carbon $to = null, ?string $crmProfileId = null): int‹..= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING]© CoachingFeedbackCoachUserln.php Xstohedeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch Filterder1n1t1on:› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 Ф >34 0 >45 đ >136 @t›146 C>151 C1usadeorivate const int No GROUP 10 = 999orivate UserRenository suserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection{...hpublic function toArray@: array{...;private function getOptions(): array{...}public function getValue: array{...}private function getDefaultValue: array{...}public function aetValidationRules(2strina Sorefix = null): arravs...?public function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}* [Platform] Refinemen... 46 m left100% 1• Mon 18 May 16:14:28AskJiminnyReportActivityServiceTest vCascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSCIM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for methodw9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ATTR_ ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabledl•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/Ueer/M PoleAttr.nhn +17-ites/User/ ẞ RoleAttrTest.nhn +224* Reiect alliiAccent alliAsk anvthina (&4-L)« Code SWF-1.6WN Windsurf Toams 212-21 UTF.8io 4 spaces...
|
NULL
|
4142045126935749218
|
NULL
|
click
|
ocr
|
NULL
|
PhostormVIewINavicarecodeFV faVsco.js°9 master kro PhostormVIewINavicarecodeFV faVsco.js°9 master kroledeym RinaCentralVideo> M SalestorceIm Salesloft> D Talkdeskureams> D Telus>D Twilio_ IWillorlex_ I willorlexDireet• _ I Willo Video_Uploader› _ Vonagewxant1400mZoomBot> ZoomPhoneC) ActivitvcrmFieldsResolver.ohpC) ActivitvLoaService.oho© ActivitvProviderClient.ohp(C) ActivitvProviderRedistrv.oho© ActivityProviderService.php© CallDenormalizerRegistry.phpC) [EMAIL]© DatalmportHandlerInterface.php© MeetingBotService.php© ParticipantConsentService.php© ParticipantsService.phpT ResponseValidationTrait.phpT SalesforceGetUserTrait.php- S/DenormaliserMainCrmDatalrait.onp@ TrackRecordinoFllesizeservice.one© TrackRecordingSizeEnforcer.phpT ValidateEmitProspectEventTrait.phpC AjReports0 AvatarMColondar0 Conference0 Crm> C Bullhornv closeOnoortunitvsvncstrateav• ProcessonProspectSearchStrateav• M TranslatorC) Client.ohr) CloseExcertion.ohoC) FieldDefinitions.onvc) SieldValueConverter ohn•) Service nhnC) StandardFioldMetadata nhn> MConnen© SoftPhoneManager.phpC) CoreUserRequest.pnp© CoreUser.php©Crm/…../Service.php Xclass Service extends BaseService 1mpLementsmA6лv169 C184 0189 0 >194 0213 0222 0>244 6t>* ainherxcdocpublic function getDefaultActivityField(string $activityType): Field{...}* dinherzzdocpublic function getsupportedPlavbooklvpeso: arrav-...public function getbealinsightsFields: arrayl...protected function getbefaultFolLowupLayoutFields(string sactivitylype): array....public function syncFields: voidSthis->svncStandardFieldsoSthis->svncCustomFieldso* @important Works only for custom fieldspublic function syncField(Field $field): void{...}Zusagesprivate function isCustomField(string $fieldId): boolf...}* @inheritdocpublic function importPicklistValues(Field Sfield): array{...}l* oimoortant "e ontu suooort stades on the obdortunitu obtecti* Qoaram strinalllnull Stunesnublic function importStages(2arnav Stvnes = null. ?strina SmissinaStageName = null): 2Stage{...}* Ginhenitdocpublic function syncLeads(Carbon Ssince, ?Carbon $to = null, ?string $crmProfileId = null): int‹..= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING]© CoachingFeedbackCoachUserln.php Xstohedeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch Filterder1n1t1on:› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 Ф >34 0 >45 đ >136 @t›146 C>151 C1usadeorivate const int No GROUP 10 = 999orivate UserRenository suserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection{...hpublic function toArray@: array{...;private function getOptions(): array{...}public function getValue: array{...}private function getDefaultValue: array{...}public function aetValidationRules(2strina Sorefix = null): arravs...?public function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}* [Platform] Refinemen... 46 m left100% 1• Mon 18 May 16:14:28AskJiminnyReportActivityServiceTest vCascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSCIM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for methodw9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ATTR_ ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabledl•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/Ueer/M PoleAttr.nhn +17-ites/User/ ẞ RoleAttrTest.nhn +224* Reiect alliiAccent alliAsk anvthina (&4-L)« Code SWF-1.6WN Windsurf Toams 212-21 UTF.8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
54538
|
1896
|
60
|
2026-05-18T13:14:31.601013+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110071601_m1.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - [Platform] Refinement 🔍
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - [Platform] Refinement 🔍","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true}]...
|
-200939925990973835
|
-260678276249952170
|
visual_change
|
hybrid
|
NULL
|
Meet - [Platform] Refinement 🔍
FirefoxFileEditView Meet - [Platform] Refinement 🔍
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp§ [Platform] Refinemen... 46 m left100% C8• Mon 18 May 16:14:31meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com=Nikolay Yankov (Presenting)+Mon 18 May 16:14**(18® For you© Recent|# Starred8? Apps07 SpacesSaarco(9 Service-DeskJiminny (Old)Jiminny (New)+--1 CD Platform TeamIID Capture Team |IID Enterprise Stabllity L..(ID Processing TeamIID SE Kanban|= More spaces=FiltersB Dashboards® Operations83 Confluence3: Team-% Customise sidebarC Atte© ours* CiauCostaooxC AlovPipe© 3m0 sml• JmiZ tY-F Prolhttps://fminny.atlassian.net/browse/JY-20879C Projects|• Home | SalesforceE Datadog* Cauời*xIinsights & Coachin.C Dev- UX+ CreateAsk RovoSpaces Jminty (New)Add parent#JY-20679Enable users to use their new activity typesBackiogIE Improve Story|~ DetailsDescriptionCurrently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomabons that we have - this means they neod to copy an or their prompes egan. wnich takes a nuge amourk or ürne.Assignee8 UnassignedAssign to me• when a new activity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to all the playbooks where activity type field isKUpOrtEGalya DimitrovaChate• it a customer marually creates an activity type in Jiminny - show them a warning message on the top saying "If you want this activity type to syng to your CRM oneach call - make sure to have i& in your CRM with Ine exact same name* - use this https://www.figma.com/design/XcUely9mc5Fiz8KosLAUn/Project-Phoenc0aode-id=7483-2600188t-uJOZVbpzYIpC5PAy-1 Connect your figma aec• the message should stay only while they are adding the activity type - If they leave or refresh the page it should disappear. Use the info message that we have• Quick start developmentLink this work item to your code byIncluding keys when creatingabranch, commit, or pull requestSubtasksDismissAdd sudkasx@ Open with VS CodeLinked work itemsCreate branchiAdd linked work item* Create commitlActivityComponentsPlatformWork logAdd a comment..Suggest a reply.…Who is working on this…..?Status update…Pro tip: press (M. 10 commertNoneStory point estimatoNikolay YankovNikolay IvanovAneliya AngelovaGalya DimitrovaTurn on microphone (88 + d)4:14 PM | [Platform] Refinement ®Lukas Kovalik1:00:58Lộ3...
|
54536
|
NULL
|
NULL
|
NULL
|
|
54539
|
1896
|
61
|
2026-05-18T13:14:35.454308+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110075454_m1.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
6
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Zoom in
Open in new window
Enter Full Screen
Nikolay Yankov
Nikolay Ivanov
Aneliya Angelova
Galya Dimitrova
Lukas Kovalik
Others might see more of your background. Click to view your full video.
4:14
PM
[Platform] Refinement 🔍
[Platform] Refinement 🔍
Audio settings
Turn on microphone
Video settings
Turn off camera...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - [Platform] Refinement 🔍","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.8411111,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.0,"top":0.8794444,"width":0.033680554,"height":0.03888889},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.0,"top":0.91833335,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.95666665,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Nikolay Yankov (Presenting)","depth":12,"bounds":{"left":0.07534722,"top":0.101111114,"width":0.124305554,"height":0.022222223},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Yankov (Presenting)","depth":13,"bounds":{"left":0.07534722,"top":0.10222222,"width":0.124305554,"height":0.020555556},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.88680553,"top":0.08944444,"width":0.04097222,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":22,"bounds":{"left":0.9145833,"top":0.101111114,"width":0.0048611113,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Take notes with Gemini","depth":14,"bounds":{"left":0.93333334,"top":0.08944444,"width":0.025,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Take notes with Gemini","depth":17,"bounds":{"left":0.9361111,"top":0.101111114,"width":0.06388891,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Gemini","depth":17,"bounds":{"left":0.96666664,"top":0.101111114,"width":0.028125,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":17,"bounds":{"left":0.96458334,"top":0.090555556,"width":0.023611112,"height":0.037777778},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Zoom in","depth":13,"bounds":{"left":0.63090277,"top":0.78333336,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open in new window","depth":13,"bounds":{"left":0.6642361,"top":0.78333336,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Enter Full Screen","depth":13,"bounds":{"left":0.69756943,"top":0.78333336,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Nikolay Yankov","depth":17,"bounds":{"left":0.753125,"top":0.36277777,"width":0.07673611,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":17,"bounds":{"left":0.87951386,"top":0.36277777,"width":0.07395833,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":17,"bounds":{"left":0.753125,"top":0.6205556,"width":0.088541664,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Galya Dimitrova","depth":17,"bounds":{"left":0.87951386,"top":0.6205556,"width":0.08194444,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Lukas Kovalik","depth":17,"bounds":{"left":0.753125,"top":0.87833333,"width":0.06875,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Others might see more of your background. Click to view your full video.","depth":14,"bounds":{"left":0.96631944,"top":0.875,"width":0.018055556,"height":0.028888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4:14","depth":12,"bounds":{"left":0.050347224,"top":0.9444444,"width":0.021527778,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PM","depth":12,"bounds":{"left":0.07534722,"top":0.9444444,"width":0.016666668,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"[Platform] Refinement 🔍","depth":12,"bounds":{"left":0.109375,"top":0.9111111,"width":0.12881945,"height":0.08888888},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[Platform] Refinement 🔍","depth":15,"bounds":{"left":0.109375,"top":0.9438889,"width":0.12881945,"height":0.023333333},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Audio settings","depth":13,"bounds":{"left":0.32118055,"top":0.9288889,"width":0.06111111,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn on microphone","depth":13,"bounds":{"left":0.34895834,"top":0.9288889,"width":0.033333335,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":true,"is_selected":false},{"role":"AXButton","text":"Video settings","depth":13,"bounds":{"left":0.38784721,"top":0.9288889,"width":0.06111111,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn off camera","depth":13,"bounds":{"left":0.415625,"top":0.9288889,"width":0.033333335,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
1310605054997823888
|
2017748268687355958
|
visual_change
|
accessibility
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
6
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Zoom in
Open in new window
Enter Full Screen
Nikolay Yankov
Nikolay Ivanov
Aneliya Angelova
Galya Dimitrova
Lukas Kovalik
Others might see more of your background. Click to view your full video.
4:14
PM
[Platform] Refinement 🔍
[Platform] Refinement 🔍
Audio settings
Turn on microphone
Video settings
Turn off camera...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
54541
|
1897
|
34
|
2026-05-18T13:14:41.469676+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110081469_m2.jpg...
|
PhpStorm
|
faVsco.js – Crm/…/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PnostormVIewINavicatecodeFV faVsco.js°9 master kPr PnostormVIewINavicatecodeFV faVsco.js°9 master kProiect vRinaCentralVideo• SalesforceIm Salesloft> D Talkdeskureams> D Telus>D Twilio_ IWillorlex_ IwilorlexDirect• _ I willoVideo_Uploader› _ VonagewxantDZ00ПZoomBot> ZoomPhoneC) ActivitvcrmFieldsResolver.ohpC) ActivitvLoaService.oho© ActivitvProviderClient.ohp(C) ActivitvProviderRedistrv.oho© ActivityProviderService.php© CallDenormalizerRegistry.phpC) [EMAIL]© DatalmportHandlerInterface.php© MeetingBotService.php© ParticipantConsentService.php(c) DarticinanteService nhnT PecnonceValidation Trait nhnT SalesforceGetUserTrait.php- S/DenormaliserMainCrmDatalrait.onp@ TrackRecordinoFllesizeservice.one(c) TrackRecordinaSizeEntorcer.onpT ValidateEmitProspectEventTrait.phpC AjReports0 AvatarMcolondar0 Conference0 Crm> C Bullhornv closeOnoortunitvsvncstrateav• ProcessonProspectSearchStrateav• M TranslatorC) Client.oho) CloseExcertion.ohoC) FieldDefinitions.onvc) SieldValueConverter ohn•) Service nhnC) StandardFioldMetadata nhn> MConnen© SoftPhoneManager.phpC) CoreUserRequest.onp© CoreUser.php© Crm/../Service.php xclass Service extends BaseService 1mpLementsm A8 A39 M5 лV169 @184 0189 0 >194 0213 0222 0>244 6t>295 (б1* ainherxcdocpublic function getDefaultActivityField(string $activityType): Field{...}* oinnerztdocpublic function getsupportedPlavbooklvpeso: arnav-...public function getbealinsightsFields: arrayl...protected function getbefaultFolLowupLayoutFields(string sactivitylype): arrayi...public function syncFields: voidSthis->svncStandardFieldsoSthis->svncCustomFieldso* @important Works only for custom fieldspublic function syncField(Field $field): void{...}Zusagesprivate function isCustomField(string $fieldId): bool{...}* Oinheritdocpublic function importPicklistValues(Field Sfield): array{...}l* oimoortant "e ontu suooort stades on the obdortunitu obtecti* dparam strinalnul Stuoespublie function importStages(Parnay Stvnes = null. Istrina SmissinaStageName = null)e eStager....-* Oinheritdocpublic function syncLeads(Carbon Ssince, ?Carbon $to = null, ?string $crmProfileId = null): int‹..= custom.log= laravel.log« SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]& console [EU]A console [STAGING]© CoachingFeedbackCoachUserln.php Xstohedeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 Ф >34 0 >45 0г >'118-131?135136 @t›— 145-1460)150151 G>usadeorivate const int No GROUP 10 = 999orivate UserRenository suserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection{...hpublic function toArray: array{...hprivate function getOptions(): array{...}public function getValue®: array{...}private function getDefaultValue: array{...}public function aetValidationRules(2strina Sorefix = null): arrav{...?lpublic function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}40m lel• мon 10 Mаy 10.14.40cascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSClM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for method9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ATTR_ ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabledl•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/Ueer/M PoleAttr.nhn +17-ites/User/ ẞ RoleAttrTest.nhn +224* Reiect allAccent alliAsk anvthina (&4-L)« Code SWF-1.6W Windsurf Teams 212-21UTF.Afo 4 spaces...
|
NULL
|
-6231794697482593788
|
NULL
|
click
|
ocr
|
NULL
|
PnostormVIewINavicatecodeFV faVsco.js°9 master kPr PnostormVIewINavicatecodeFV faVsco.js°9 master kProiect vRinaCentralVideo• SalesforceIm Salesloft> D Talkdeskureams> D Telus>D Twilio_ IWillorlex_ IwilorlexDirect• _ I willoVideo_Uploader› _ VonagewxantDZ00ПZoomBot> ZoomPhoneC) ActivitvcrmFieldsResolver.ohpC) ActivitvLoaService.oho© ActivitvProviderClient.ohp(C) ActivitvProviderRedistrv.oho© ActivityProviderService.php© CallDenormalizerRegistry.phpC) [EMAIL]© DatalmportHandlerInterface.php© MeetingBotService.php© ParticipantConsentService.php(c) DarticinanteService nhnT PecnonceValidation Trait nhnT SalesforceGetUserTrait.php- S/DenormaliserMainCrmDatalrait.onp@ TrackRecordinoFllesizeservice.one(c) TrackRecordinaSizeEntorcer.onpT ValidateEmitProspectEventTrait.phpC AjReports0 AvatarMcolondar0 Conference0 Crm> C Bullhornv closeOnoortunitvsvncstrateav• ProcessonProspectSearchStrateav• M TranslatorC) Client.oho) CloseExcertion.ohoC) FieldDefinitions.onvc) SieldValueConverter ohn•) Service nhnC) StandardFioldMetadata nhn> MConnen© SoftPhoneManager.phpC) CoreUserRequest.onp© CoreUser.php© Crm/../Service.php xclass Service extends BaseService 1mpLementsm A8 A39 M5 лV169 @184 0189 0 >194 0213 0222 0>244 6t>295 (б1* ainherxcdocpublic function getDefaultActivityField(string $activityType): Field{...}* oinnerztdocpublic function getsupportedPlavbooklvpeso: arnav-...public function getbealinsightsFields: arrayl...protected function getbefaultFolLowupLayoutFields(string sactivitylype): arrayi...public function syncFields: voidSthis->svncStandardFieldsoSthis->svncCustomFieldso* @important Works only for custom fieldspublic function syncField(Field $field): void{...}Zusagesprivate function isCustomField(string $fieldId): bool{...}* Oinheritdocpublic function importPicklistValues(Field Sfield): array{...}l* oimoortant "e ontu suooort stades on the obdortunitu obtecti* dparam strinalnul Stuoespublie function importStages(Parnay Stvnes = null. Istrina SmissinaStageName = null)e eStager....-* Oinheritdocpublic function syncLeads(Carbon Ssince, ?Carbon $to = null, ?string $crmProfileId = null): int‹..= custom.log= laravel.log« SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]& console [EU]A console [STAGING]© CoachingFeedbackCoachUserln.php Xstohedeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 Ф >34 0 >45 0г >'118-131?135136 @t›— 145-1460)150151 G>usadeorivate const int No GROUP 10 = 999orivate UserRenository suserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection{...hpublic function toArray: array{...hprivate function getOptions(): array{...}public function getValue®: array{...}private function getDefaultValue: array{...}public function aetValidationRules(2strina Sorefix = null): arrav{...?lpublic function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}40m lel• мon 10 Mаy 10.14.40cascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSClM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for method9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ATTR_ ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabledl•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/Ueer/M PoleAttr.nhn +17-ites/User/ ẞ RoleAttrTest.nhn +224* Reiect allAccent alliAsk anvthina (&4-L)« Code SWF-1.6W Windsurf Teams 212-21UTF.Afo 4 spaces...
|
54537
|
NULL
|
NULL
|
NULL
|
|
54540
|
1896
|
62
|
2026-05-18T13:14:41.489249+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110081489_m1.jpg...
|
PhpStorm
|
faVsco.js – Crm/…/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp§ [Platform] Refinemen... 46 m left100% C8• Mon 18 May 16:14:41meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com=Nikolay Yankov (Presenting)+Mon 18 May 16:14*(18® For you© Recent|# Starred8? Apps0 Spacesauarco(9 Service-DeskJiminny (Oid)Jiminny (New)+--1 CD Platform TeamIID Capture Team |IID Enterprise Stabllity L..(ID Processing TeamIID SE Kanban= More spaces=Filters(B Dashboards® OperationsE3 Confluence?: Teamd% Customise sidebarC Atte* 0%CossaoouAlolPipe0 3m© sml• Jmi1 tY-https://fiminny.atlassian.net/browse/JY-2087923 SSH• Home | Salestorce* Claudel*xI+ CreateSpacesJminty (New)Add parent-JY-20679Enable users to use their new activity types• DescriptionCurrently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomacons that we have - this means they need to copy ar or their prompes egan. wnich takes a nuge amourk or üreFor SF, Hubspot, Copper, Bullhorn, Close and Pipedrive (anything apart from Zoho)• when a new activity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to all the playbooks where activity type field isChetu• if a customer manualy creates an activity type in Jiminny - show them a warning message on the top saying "If you want this activity type to sync to your CRM oneach call - make sure to have it in your CRM with the exact same name" - use this https://www.figma.com/design/XcUety9mxSFiz8KosLAUn/Project-Phoenb?node-id=7483-2600188t-uJOZVbpzYIpC5PAy-1 [Connect your Fioma ascount• the message should stay only while they are adding the activity type - Il they leave or refresh the page it should disappear. Use the info message that we haveSubtasksAdd subtaskLinked work itemsAdd linked work item~ ActivityWork logAeaconmeneSuggest a reply.….Who is working on this...?Status update…Pro tip: press (M to comment7 tY-F ProlInsights & CoachinC DevAsk RovoBackiogIX Improve StoryDetailsAssignee8 UnassignedAssign to meKUpOrtE& Galya Dimitrova• Quick start developmentLink this work item to your code byincluding keys when creating abranch, commit, or pull requestDismiss@ Open with VS CodeCreate branch* Create commit)ComponentsPiatformNomkStory point estimatoNikolay YankovNikolay IvanovAneliya Angelovaalya Dimitrova4:14 PM | [Platform] Refinement ®Lukas Kovalik1:01:08IllLộ3...
|
NULL
|
8239185329869302114
|
NULL
|
click
|
ocr
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp§ [Platform] Refinemen... 46 m left100% C8• Mon 18 May 16:14:41meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com=Nikolay Yankov (Presenting)+Mon 18 May 16:14*(18® For you© Recent|# Starred8? Apps0 Spacesauarco(9 Service-DeskJiminny (Oid)Jiminny (New)+--1 CD Platform TeamIID Capture Team |IID Enterprise Stabllity L..(ID Processing TeamIID SE Kanban= More spaces=Filters(B Dashboards® OperationsE3 Confluence?: Teamd% Customise sidebarC Atte* 0%CossaoouAlolPipe0 3m© sml• Jmi1 tY-https://fiminny.atlassian.net/browse/JY-2087923 SSH• Home | Salestorce* Claudel*xI+ CreateSpacesJminty (New)Add parent-JY-20679Enable users to use their new activity types• DescriptionCurrently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomacons that we have - this means they need to copy ar or their prompes egan. wnich takes a nuge amourk or üreFor SF, Hubspot, Copper, Bullhorn, Close and Pipedrive (anything apart from Zoho)• when a new activity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to all the playbooks where activity type field isChetu• if a customer manualy creates an activity type in Jiminny - show them a warning message on the top saying "If you want this activity type to sync to your CRM oneach call - make sure to have it in your CRM with the exact same name" - use this https://www.figma.com/design/XcUety9mxSFiz8KosLAUn/Project-Phoenb?node-id=7483-2600188t-uJOZVbpzYIpC5PAy-1 [Connect your Fioma ascount• the message should stay only while they are adding the activity type - Il they leave or refresh the page it should disappear. Use the info message that we haveSubtasksAdd subtaskLinked work itemsAdd linked work item~ ActivityWork logAeaconmeneSuggest a reply.….Who is working on this...?Status update…Pro tip: press (M to comment7 tY-F ProlInsights & CoachinC DevAsk RovoBackiogIX Improve StoryDetailsAssignee8 UnassignedAssign to meKUpOrtE& Galya Dimitrova• Quick start developmentLink this work item to your code byincluding keys when creating abranch, commit, or pull requestDismiss@ Open with VS CodeCreate branch* Create commit)ComponentsPiatformNomkStory point estimatoNikolay YankovNikolay IvanovAneliya Angelovaalya Dimitrova4:14 PM | [Platform] Refinement ®Lukas Kovalik1:01:08IllLộ3...
|
54539
|
NULL
|
NULL
|
NULL
|
|
54542
|
1896
|
63
|
2026-05-18T13:14:44.470451+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110084470_m1.jpg...
|
PhpStorm
|
faVsco.js – Crm/…/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp[Platform) Refinemen…46 m lmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.comNikolay Yankov (Presenting)• Mon 18 May 16:14:436Mon 18 May 16:14**(18® For you© Recent|# Starred8? Apps07 SpacesSaarco(9 Service-DeskJiminny (Oid)Jiminny (New)+--I CD Platform TeamIID Capture Team|IID Enterprise Stabllity L.(ID Processing TeamCID SE Kanban= More spaces=Filters(B Dashboards® OperationsE3 Confluence3: Team-% Customise sidebarC Atte* 0%CossaoouAlolPipe© Jmi |oJiml23 SSH• Home | Salestorce* ClaudeSentry*xI+ CreateSpacesJminty (New)Add parent#JY-20679Enable users to use their new activity types• DescriptionCurrently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomacons that we have - this means they need to copy ar or their prompes egan. wnich takes a nuge amourk or üreFor SF, Hubspot, Copper, Bullhorn, Close and Pipedrive (anything apart from Zoho)• when a new activity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to all the playbooks where activity type field isCheterd• if a customer manualy creates an activity type in Jiminny - show them a warning message on the top saying "If you want this activity type to sync to your CRM oneach call - make sure to have i& in your CRM with the exact same name* - use this F https://www.figma.com/design/XcUety9mxSFiz8KosLAUn/Project-Phoenb?node-id=7483-2600188t-uJOZVbpzYIpC5PAy-1 [Connect your Fioma account• the message should stay only while they are adding the activity type - Il they leave or refresh the page it should disappear. Use the info message that we haveSubtasksAdd subtaskLinked work itemsAdd linked work item~ ActivityWork logSuggest a reply.….Who is working on this...?Status update…Pro tip: press (M to comment7 tY-F ProlInsights & CoachinC DevAsk RovoBackiogIX Improve Story~ DetailsAssignee8 UnassignedAssign to meKUpOrtE& Galya Dimitrova• Quick start developmentLink this work item to your code byincluding keys when creating abranch, commit, or pull requestDismissyeoemer@ Open with VS CodeCreate branch* Create commit)ComponentsPlatformSo-rtoNomkStory point estimatoNikolay YankovNikolay IvanovAneliya Angelovaalya DimitrovaLukas Kovalik1:01:114:14 PM | [Platform] Refinement ®Lộ3...
|
NULL
|
5614435375578836636
|
NULL
|
visual_change
|
ocr
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp[Platform) Refinemen…46 m lmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.comNikolay Yankov (Presenting)• Mon 18 May 16:14:436Mon 18 May 16:14**(18® For you© Recent|# Starred8? Apps07 SpacesSaarco(9 Service-DeskJiminny (Oid)Jiminny (New)+--I CD Platform TeamIID Capture Team|IID Enterprise Stabllity L.(ID Processing TeamCID SE Kanban= More spaces=Filters(B Dashboards® OperationsE3 Confluence3: Team-% Customise sidebarC Atte* 0%CossaoouAlolPipe© Jmi |oJiml23 SSH• Home | Salestorce* ClaudeSentry*xI+ CreateSpacesJminty (New)Add parent#JY-20679Enable users to use their new activity types• DescriptionCurrently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomacons that we have - this means they need to copy ar or their prompes egan. wnich takes a nuge amourk or üreFor SF, Hubspot, Copper, Bullhorn, Close and Pipedrive (anything apart from Zoho)• when a new activity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to all the playbooks where activity type field isCheterd• if a customer manualy creates an activity type in Jiminny - show them a warning message on the top saying "If you want this activity type to sync to your CRM oneach call - make sure to have i& in your CRM with the exact same name* - use this F https://www.figma.com/design/XcUety9mxSFiz8KosLAUn/Project-Phoenb?node-id=7483-2600188t-uJOZVbpzYIpC5PAy-1 [Connect your Fioma account• the message should stay only while they are adding the activity type - Il they leave or refresh the page it should disappear. Use the info message that we haveSubtasksAdd subtaskLinked work itemsAdd linked work item~ ActivityWork logSuggest a reply.….Who is working on this...?Status update…Pro tip: press (M to comment7 tY-F ProlInsights & CoachinC DevAsk RovoBackiogIX Improve Story~ DetailsAssignee8 UnassignedAssign to meKUpOrtE& Galya Dimitrova• Quick start developmentLink this work item to your code byincluding keys when creating abranch, commit, or pull requestDismissyeoemer@ Open with VS CodeCreate branch* Create commit)ComponentsPlatformSo-rtoNomkStory point estimatoNikolay YankovNikolay IvanovAneliya Angelovaalya DimitrovaLukas Kovalik1:01:114:14 PM | [Platform] Refinement ®Lộ3...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
54543
|
1896
|
64
|
2026-05-18T13:14:54.050099+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110094050_m1.jpg...
|
PhpStorm
|
faVsco.js – Crm/…/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
39
5
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Close;
use Cache;
use Carbon\Carbon;
use Exception;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\CloseInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\ServiceUnavailableException;
use Jiminny\Exceptions\UnexpectedCallException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Close\Processor\AccountProcessor;
use Jiminny\Services\Crm\Close\Processor\MetadataProcessor;
use Jiminny\Services\Crm\Close\Processor\OpportunityProcessor;
use Jiminny\Services\Crm\Close\Processor\StageProcessor;
use Jiminny\Services\Crm\Helpers\FilterJoinedParticipants;
use Jiminny\Services\Crm\Metadata\OpportunityMetadata;
use Jiminny\Services\Crm\Metadata\ProfileMetadata;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Sentry;
use UnexpectedValueException;
class Service extends BaseService implements
CloseInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
RemoteEntityManipulationInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
SupportsObjectTypeParseInterface,
VerifyTaskExistsInterface
{
private const int NOTE_BODY_MAX_LENGTH = 3000000;
private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day
private StandardFieldMetadata $standardFieldMetadata;
private MetadataProcessor $metadataProcessor;
private FieldValueConverter $fieldValueConverter;
private StageProcessor $stageProcessor;
private OpportunityProcessor $opportunityProcessor;
private AccountProcessor $accountProcessor;
public function __construct(
Client $client,
StandardFieldMetadata $standardFieldMetadata,
MetadataProcessor $metadataProcessor,
FieldValueConverter $fieldValueConverter,
StageProcessor $stageResolver,
OpportunityProcessor $opportunityProcessor,
AccountProcessor $accountProcessor,
private readonly ProspectPhotoPathService $prospectPhotoPathService,
) {
parent::__construct();
$this->client = $client;
$this->standardFieldMetadata = $standardFieldMetadata;
$this->metadataProcessor = $metadataProcessor;
$this->fieldValueConverter = $fieldValueConverter;
$this->stageProcessor = $stageResolver;
$this->opportunityProcessor = $opportunityProcessor;
$this->accountProcessor = $accountProcessor;
}
public function getDisplayName(): string
{
return 'Close';
}
public function setConfiguration(Configuration $config): void
{
parent::setConfiguration($config);
$this->metadataProcessor->setConfiguration($config);
$this->stageProcessor->setConfiguration($config);
$this->opportunityProcessor->setConfiguration($config);
$this->accountProcessor->setConfiguration($config);
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
return $user->getTeam()->getOwner()->getSocialAccount(SocialAccount::PROVIDER_CLOSE);
}
private function getClient(): Client
{
if (! $this->client instanceof Client) {
throw new UnexpectedCallException('Client not set');
}
return $this->client;
}
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return $this->fieldValueConverter->convertFromCrm($fieldType, $fieldValue);
}
protected function getFieldTypes(): array
{
return [
parent::OBJECT_OPPORTUNITY,
parent::OBJECT_CONTACT,
parent::OBJECT_ACCOUNT,
];
}
protected function getFields(string $crmObject): array
{
// not used
return [];
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
// Set up the activity field as the default Type.
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'type',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
public function syncFields(): void
{
$this->syncStandardFields();
$this->syncCustomFields();
}
/**
* @important Works only for custom fields
*/
public function syncField(Field $field): void
{
$resource = $this->convertObjectTypeToResource($field->getObjectType());
// We can only sync custom fields in this CRM.
if ($this->isCustomField($field->getCrmProviderId()) === false) {
return;
}
$crmField = $this->getClient()->fetchCustomFieldDefinition($resource, $field->getCrmProviderId());
$this->metadataProcessor->syncField($crmField);
}
private function isCustomField(string $fieldId): bool
{
return strpos($fieldId, 'cf_') === 0;
}
/**
* @inheritdoc
*/
public function importPicklistValues(Field $field): array
{
// handled in syncFields()
return [];
}
/**
* @important We only support stages on the opportunity object
*
* @param string[]|null $types
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
if (! $missingStageName) {
// This is taken care of by syncOrganization()
return null;
}
$stage = $this->stageProcessor->resolveFromStageId($missingStageName);
if ($stage instanceof Stage) {
return $stage;
}
$stageMetadata = $this->getClient()->fetchStage($missingStageName);
if (! $stageMetadata) {
$this->logger->error('Stage does not exist', [
'stage' => $missingStageName,
]);
return null;
}
return $this->stageProcessor->importStage($stageMetadata);
}
/**
* @inheritdoc
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Even though Close.io has the concept of "leads", they fit more into our concept of accounts.
return 0;
}
/**
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Not a supported entity.
return null;
}
/**
* @throws Exception
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
foreach ($this->getClient()->listAccounts($since) as $clAccount) {
// Only sync if previously imported.
if ($this->hasAccount($clAccount->getId())) {
$this->importAccount($clAccount);
$syncCount++;
}
}
} catch (Exception $exception) {
$this->logger->error('Account sync failed', [
'error' => $exception->getMessage(),
]);
throw $exception;
}
return $syncCount;
}
public function syncAccount(string $crmId): ?Account
{
return $this->accountProcessor->syncAccount($crmId);
}
private function importAccount($crmData): Account
{
return $this->accountProcessor->importAccountMetadata($crmData);
}
/**
* @throws CloseException
*/
public function syncOpportunities(array $parameters, ?string $strategy = null): int
{
$strategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$strategies = $strategyResolver->getStrategies($this->config, $strategy);
$syncCount = 0;
try {
$opportunities = [];
foreach ($strategies as $syncStrategy) {
$opportunitiesData = $syncStrategy->fetchOpportunities($parameters);
$opportunities[] = $opportunitiesData['data'];
if ($opportunitiesData['has_more']) {
$this->logger->info('[Close] Sync Opportunities - count warning', [
'team_id' => $this->config->getTeam()->getId(),
'total' => $opportunitiesData['total'],
'count' => $opportunitiesData['count'],
'skip' => $opportunitiesData['skip'],
'strategies_count' => count($strategies),
]);
}
}
$opportunities = array_merge(...$opportunities);
} catch (CrmException $exception) {
$this->logger->error('Fetching opportunity data failed', [
'team' => $this->getTeam()->getSlug(),
'error' => $exception->getMessage(),
]);
return 0;
}
foreach ($opportunities as $opportunityMetadata) {
try {
$this->importOpportunity($opportunityMetadata);
$syncCount++;
} catch (Exception $exception) {
$this->logger->warning('Opportunity sync failed', [
'opportunity' => $opportunityMetadata->getId(),
'error' => $exception->getMessage(),
]);
}
}
return $syncCount;
}
public function syncOpportunity(string $crmId): ?Opportunity
{
$strategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$strategy = $strategyResolver->resolve(
$this->config,
OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY,
);
$parameters = ['crm_id' => $crmId];
$opportunity = $strategy->fetchOpportunities($parameters);
if (empty($opportunity['data'])) {
return null;
}
return $this->importOpportunity($opportunity['data']);
}
private function importOpportunity(OpportunityMetadata $crmData): ?Opportunity
{
if (! $crmData->getLeadId()) {
$this->logger->warning('Opportunity does not have a lead ID', [
'opportunity' => $crmData->getId(),
]);
return null;
}
$account = $this->getConfiguration()
->accounts()
->where('crm_provider_id', $crmData->getLeadId())
->first();
if ($account === null) {
$account = $this->accountProcessor->syncAccount($crmData->getLeadId());
}
/** @var Profile $profile */
$profile = $this->getConfiguration()
->profiles()
->where('crm_provider_id', $crmData->getUserId())
->first();
$userId = $profile?->getUserId() ?? $account?->getUserId();
if ($userId === null) {
$this->logger->error('[Close] | Skip import, no user_id found', [
'id' => $crmData->getId(),
]);
return null;
}
$stage = $this->getConfiguration()
->stages()
->where('crm_provider_id', $crmData->getStageId())
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $crmData->getStageId());
}
return $this->opportunityProcessor->import($crmData, $account, $stage, $profile);
}
/**
* @param array<string,string> $crmData
* @param string[] $crmFields
*/
public function importOpportunityCrmFieldData(array $crmData, array $crmFields, int $opportunityId): void
{
// handled in importOpportunity
}
/**
* @inheritdoc
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
/** No way to sync today.
$clContacts = $this->client->get('lead', [
'date_updated__gte' => $since->toDateString(),
'_order_by' => '-date_updated',
]);
foreach ($clContacts as $clContact) {
// Only sync if previously imported.
if ($this->hasContact($clContact['id'])) {
$this->importContact($clContact);
$syncCount++;
}
}
**/
} catch (Exception $exception) {
// Do nothing for now.
throw $exception;
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
$clContact = $this->client->get('contact/' . $crmId);
} catch (HttpNotFoundException $exception) {
return null;
}
return $this->importContact($clContact);
}
/**
* @inheritdoc
*/
private function importContact($crmData): Contact
{
$account = null;
if ($crmData['lead_id']) {
$account = $this->team
->accounts()
->where('crm_provider_id', $crmData['lead_id'])
->first();
if ($account === null) {
$account = $this->syncAccount($crmData['lead_id']);
}
}
$mobilePhone = $parsedNumber = null;
foreach ($crmData['phones'] as $phoneNumber) {
if ($phoneNumber['type'] === 'mobile') {
$mobilePhone = $this->buildContactMobilePhone(null, $phoneNumber['phone']);
} else {
$parsedNumber = $this->buildContactPhone(null, $phoneNumber['phone']);
}
}
$email = null;
if (empty($crmData['emails']) === false) {
$email = mb_strimwidth($crmData['emails'][0]['email'], 0, 80);
}
$profile = $this->config->profiles()->where('crm_provider_id', (string) $crmData['updated_by'])->first();
$data = [
'account_id' => $account->id ?? null,
'team_id' => $this->team->id,
'user_id' => $profile?->user_id,
'owner_id' => $crmData['updated_by'],
'name' => $crmData['name'] ?? 'Unknown',
'email' => $email,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobilePhone ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'title' => mb_strimwidth($crmData['title'] ?? '', 0, 128),
'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(
crmConfiguration: $this->config,
crmProviderId: $crmData['id'],
modelType: Contact::class,
fileName: $crmData['id'],
avatarText: $crmData['name'] ?? 'Unknown'
),
'remotely_created_at' => Carbon::parse($crmData['date_created']),
];
/** @var Contact */
return $this->config->contacts()->updateOrCreate(['crm_provider_id' => (string) $crmData['id']], $data);
}
private function buildContactPhone(?string $countryCode, ?string $number): ?array
{
if ($number) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($number, 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
return $parsedNumber;
}
private function buildContactMobilePhone(?string $countryCode, ?string $number): ?string
{
return $number ? mb_strimwidth(phone_e164($countryCode, $number), 0, 25) : null;
}
public function syncOrganization(): void
{
$organisation = $this->getClient()->fetchOrganisation();
$this->metadataProcessor->syncOrganisation($organisation);
foreach ($organisation->getPipelines() as $pipelineMetadata) {
$this->metadataProcessor->syncPipeline($pipelineMetadata);
}
}
private function syncStandardFields(): void
{
// Currently we sync only opportunity fields
$stages = $this->getClient()->listStages();
foreach ($this->standardFieldMetadata->opportunity($stages) as $fieldMetadata) {
$this->metadataProcessor->syncField($fieldMetadata);
}
$this->config->save();
}
private function syncCustomFields(): void
{
foreach ($this->getFieldTypes() as $fieldType) {
$objectType = $this->convertObjectTypeToResource($fieldType);
$currentFields = $this->getClient()->listCustomFieldDefinitions($objectType);
foreach ($currentFields as $fieldMetadata) {
$this->metadataProcessor->syncField($fieldMetadata);
}
}
$this->config->save();
}
public function syncProfiles(?User $userToSearch = null): ?Profile
{
/*
* Fetch the profile of the user from the database
* Then fetch the user metadata from Close and update it
* In case there's no profile for the user, proceed with syncing all users
*/
$foundUser = null;
if ($userToSearch) {
$profile = $userToSearch->getProfile();
if ($profile instanceof Profile) {
$crmProviderId = $profile->getCrmProviderId();
if ($crmProviderId) {
$profileMetadata = $this->getClient()->fetchUser($crmProviderId);
if (! $profileMetadata instanceof ProfileMetadata) {
return null;
}
return $this->metadataProcessor->syncProfile($profileMetadata);
}
}
}
foreach ($this->getClient()->listUsers() as $userMetadata) {
$userProfile = $this->metadataProcessor->syncProfile($userMetadata);
if (
$userToSearch instanceof User
&& $userProfile instanceof Profile
&& $userProfile->getUserId() === $userToSearch->getId()
) {
$foundUser = $userProfile;
}
}
return $foundUser;
}
public function syncProfileFields(): void
{
// Not used.
}
/**
* @inheritdoc
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
$data = Cache::remember($this->team->id . $name . $count . $offset, 300, function () use ($name, $count, $offset) {
$data = [];
try {
// If search phrase resembles phone number remove special symbols
if (preg_match('/^([0-9\s\-\+\(\)]*)$/', $name)) {
$name = '+' . preg_replace('/[\s\-\+\(\)]/', '', $name);
}
// Close do not provide a unified way to search, so we must hack our own.
$objects = $this->client->get('lead', [
'query' => 'sort:date_updated name:(' . $name . ') or email:(' . $name . ') or phone:(' . $name . ')',
'_limit' => $count, '_skip' => $offset,
]);
} catch (\GuzzleHttp\Exception\ServerException $exception) {
throw new ServiceUnavailableException($exception->getMessage());
}
foreach ($objects['data'] as $object) {
// We need a contact to dial it.
if (empty($object['contacts'])) {
continue;
}
foreach ($object['contacts'] as $contact) {
$record = [
'crmId' => $contact['id'],
'crmUrl' => $this->generateProviderUrl($object['id'], 'account'),
'name' => $contact['name'],
'industry' => null,
'title' => $contact['title'],
'organization' => $object['display_name'],
'prospectType' => 'contact',
'phoneNumbers' => [],
];
foreach ($contact['phones'] as $phone) {
if ($phone['type'] === 'mobile') {
$number = $this->buildContactMobilePhone(null, $phone['phone']);
$record['phoneNumbers'][] = [
'number' => $number,
'nationalFormat' => phone_national(null, $number),
'type' => 'mobile',
];
} else {
$parsedNumber = $this->buildContactPhone(null, $phone['phone']);
// Add phone number to record.
if (empty($parsedNumber['phone']) === false) {
$record['phoneNumbers'][] = [
'number' => $parsedNumber['phone'],
'nationalFormat' => phone_national(null, $parsedNumber['phone']),
'type' => 'phone',
];
}
}
}
$data[] = $record;
}
}
return $data;
});
return $data;
}
/**
* @inheritdoc
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
$contact = null;
$account = null;
if ($crmAccountId) {
$account = $this->config->accounts()->where('crm_provider_id', $crmAccountId)->first();
if ($account === null) {
$account = $this->syncAccount($crmAccountId);
}
}
if ($crmContactId) {
$contact = $this->config->contacts()->where('crm_provider_id', $crmContactId)->first();
if ($contact === null) {
$contact = $this->syncContact($crmContactId);
}
}
if ($contact || $account) {
if ($contact && $account === null) {
$account = $contact->account;
}
if ($account === null) {
return [];
}
$params = [
'lead_id' => $account->crm_provider_id,
'_order_by' => '-date_updated',
];
$onlyOpen = true;
switch ($this->config->opportunity_assignment_rule) {
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:
$params['_order_by'] = '-date_updated';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:
$params['_order_by'] = '-date_created';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:
$params['_order_by'] = 'date_created';
break;
case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:
$params['_order_by'] = '-date_updated';
$onlyOpen = false;
}
if ($onlyOpen) {
$params['status_type__in'] = 'active,won';
}
$clOpportunities = $this->client->get('opportunity', $params);
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
foreach ($clOpportunities['data'] as $clOpportunity) {
$stage = $this->config
->stages()
->where('crm_provider_id', $clOpportunity['status_id'])
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $clOpportunity['status_id']);
}
$record = [
'crmId' => $clOpportunity['id'],
'name' => $clOpportunity['note'],
'value' => currency_format($clOpportunity['value'], $clOpportunity['value_currency']),
'won' => $stage->probability === 100.00,
'closed' => $clOpportunity['status_type'] !== 'active',
'stage' => [
'id' => $stage->id_string,
'name' => $stage->name,
],
'recordType' => [],
];
if ($ownerId && isset($clOpportunity['user_id']) && $clOpportunity['user_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
$crmId = null;
if ($objectType === 'contact') {
$contact = $this->syncContact($objectId);
if ($contact && $contact->account_id) {
$crmId = $contact->account->crm_provider_id;
}
} else {
$crmId = $objectId;
}
if ($crmId) {
$clTasks = $this->client->get('task', [
'lead_id' => $crmId,
'_type' => 'lead',
'assigned_to' => $this->profile->crm_provider_id,
'is_complete' => 'false',
'_order_by' => 'date',
]);
foreach ($clTasks['data'] as $clTask) {
$data[] = [
'crmId' => $clTask['id'],
'subject' => $clTask['text'],
'due' => $clTask['date'] ?? null,
'type' => null,
];
}
}
return $data;
}
/**
* Try to find email address in CRM service
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$clObject = $this->client->get('lead', [
'query' => 'sort:updated contact(email(email:"' . $email . '"))',
'_limit' => 1,
]);
if ($clObject['total_results'] > 0) {
foreach ($clObject['data'][0]['contacts'] as $clContact) {
foreach ($clContact['emails'] as $clEmail) {
if ($email === $clEmail['email']) {
$data = $this->convertCrmData($clContact['id'], $userId);
return ! empty(array_filter($data)) ? $data : null;
}
}
}
}
return null;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
// Check if the user is internal.
$teamMember = $this->team->users()->where('phone', $phone)->exists();
// Skip the attendee if internal.
if ($teamMember === false) {
$clObject = $this->client->get('lead', [
'query' => 'sort:updated contact(' . $phone . ')',
'_limit' => 1,
]);
if ($clObject['total_results'] > 0) {
foreach ($clObject['data'][0]['contacts'] as $clContact) {
foreach ($clContact['phones'] as $clPhone) {
if ($phone === $clPhone['phone']) {
$data = $this->convertCrmData($clContact['id'], $userId);
return ! empty(array_filter($data)) ? $data : null;
}
}
}
}
}
return null;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$clObject = $this->client->get('lead', [
'query' => 'sort:updated contact(name:"' . $name . '")',
'_limit' => 1,
]);
if ($clObject['total_results'] > 0) {
foreach ($clObject['data'][0]['contacts'] as $clContact) {
if ($clContact['name'] === $name || $clContact['display_name'] === $name) {
$data = $this->convertCrmData($clContact['id'], $userId);
return ! empty(array_filter($data)) ? $data : false;
}
}
}
return false;
});
return is_array($result) ? $result : null;
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
private function convertCrmData(string $crmId, ?int $userId = null): array
{
$lead = null;
$opportunity = null;
$account = null;
$stage = null;
$countryCode = null;
$contact = $this->syncContact($crmId);
if ($contact) {
$account = $contact->account;
if ($contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account) {
$countryCode = $account->country_code;
}
try {
$cpOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId,
);
if (! empty($cpOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($cpOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception) {
// Nothing to see here.
}
}
return [
$lead,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
public function saveActivity(Activity $activity): Activity
{
switch ($activity->type) {
case Activity::TYPE_CONFERENCE:
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildCallPayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
private function mapStatus(string $status): string
{
switch ($status) {
case Activity::STATUS_COMPLETED:
case Activity::STATUS_IN_PROGRESS:
case Activity::STATUS_FAILED:
case Activity::STATUS_NO_ANSWER:
case Activity::STATUS_BUSY:
default:
return $status;
case Activity::STATUS_CANCELLED:
return 'cancel';
}
}
/**
* @throws CrmException
*/
private function buildCallPayload(Activity $activity): Activity
{
try {
if ($activity->crm_provider_id) {
// The activity should be logged under the existing Task (not Activity).
$data = [
'_type' => 'lead',
'lead_id' => $activity->account->crm_provider_id,
'assigned_to' => $this->profile->crm_provider_id,
'text' => $this->generateActivityDescription($activity),
'date' => $activity->getActualEndTime()->toDateString(),
'is_complete' => true,
];
$this->logger->info('[Close CRM] Updating task', [
'activity' => $activity->id,
'crm_id' => $activity->crm_provider_id,
'data' => $data,
]);
$this->client->put('task/' . $activity->crm_provider_id, $data);
} else {
// Just create an activity.
$data = [
'lead_id' => $activity->account_id ? $activity->account->crm_provider_id : null,
'contact_id' => $activity->contact_id ? $activity->contact->crm_provider_id : null,
'created_by' => $this->profile->crm_provider_id,
'user_id' => $this->profile->crm_provider_id,
'direction' => $activity->getCrmType() === Activity::TYPE_SOFTPHONE ? 'outbound' : 'inbound',
'status' => $this->mapStatus($activity->getStatus()),
'note' => $this->generateActivityDescription($activity),
'duration' => $this->mapStatus($activity->getStatus()) !== 'cancel' ? $activity->duration : 0,
'phone' => $activity->to ? $activity->to->phone_number : null,
];
$clActivity = $this->client->post('activity/call', $data);
$this->logger->info('[Close CRM] Creating activity', [
'activity' => $activity->id,
'crm_id' => $clActivity['id'],
'data' => $data,
'response' => $clActivity,
]);
$activity->crm_provider_id = $clActivity['id'];
$activity->save();
}
} catch (ClientException $exception) {
$response = $exception->getResponse();
if ($response === null) {
// Trying to debug weird cases where this is null.
Sentry::captureException($exception);
}
$responseBody = $response->getBody();
$message = $responseBody;
$errorCode = $response->getStatusCode();
$jsonResponse = json_decode($responseBody, true);
if (isset($jsonResponse[0]['message'])) {
$message = $jsonResponse[0]['message'];
}
throw new CrmException($message, $errorCode);
}
return $activity;
}
private function buildTextMessagePayload(Activity $activity): Activity
{
$clActivity = $this->client->post('activity/sms', [
'lead_id' => $activity->account_id ? $activity->account->crm_provider_id : null,
'contact_id' => $activity->contact_id ? $activity->contact->crm_provider_id : null,
'created_by' => $this->profile->crm_provider_id,
'user_id' => $this->profile->crm_provider_id,
'status' => $activity->getType() === Activity::TYPE_SMS_OUTBOUND ? 'sent' : 'inbox',
'text' => $this->generateActivityDescription($activity),
'remote_phone' => $activity->to ? $activity->to->phone_number : null,
'local_phone' => $activity->to ? $activity->to->phone_number : null,
'source' => 'Close.io',
]);
$activity->crm_provider_id = $clActivity['id'];
$activity->save();
return $activity;
}
private function generateActivityDescription(Activity $activity): string
{
$description = '';
switch ($activity->type) {
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
case Activity::TYPE_CONFERENCE:
if ($activity->hasActivityType()) {
$description = $activity->getActivityType()->getName() . PHP_EOL . PHP_EOL;
}
if ($activity->hasTitle()) {
$description .= $activity->getTitle() . PHP_EOL;
}
if ($activity->hasReasonCodeBotKicked()) {
$description .= 'Notetaker removed from this meeting' . PHP_EOL . PHP_EOL;
// When we fix the state to be Activity::RECORDING_RECORDED as it should be this can change.
} elseif ($activity->hasReasonCodeNotCompliant()) {
$description .= 'Notetaker did not join due to recording consent not being provided by attendees' . PHP_EOL . PHP_EOL;
} elseif ($activity->canReviewActivity()) {
$playbackUrl = $activity->user->team->partner->getPlaybackUrl($activity);
$description .= 'Review Activity:' . PHP_EOL . $playbackUrl . PHP_EOL . PHP_EOL;
}
if ($activity->type === Activity::TYPE_CONFERENCE) {
$description .= 'Attendees:'
. PHP_EOL
. (new FilterJoinedParticipants())->toString($activity);
}
if (\count($activity->notes) > 0) {
$description .= PHP_EOL . PHP_EOL . 'Notes: ' . PHP_EOL;
foreach ($activity->notes as $note) {
$time = ($note->time > 3600) ? gmdate('H:i:s', $note->time) : gmdate('i:s', $note->time);
$description .= $time . ' ' . $note->note . PHP_EOL;
}
}
// Get all private messages.
$messages = $activity->messages()
->where('is_private', 1)
->orderBy('created_at', 'asc');
if ($messages->count() > 0) {
$description .= PHP_EOL . PHP_EOL . 'Coaching Chat: ' . PHP_EOL;
foreach ($messages->get() as $message) {
$description .= $message->participant->name . ': ' . $message->message . PHP_EOL;
}
}
// Get all public messages.
$messages = $activity->messages()
->where('is_private', 0)
->orderBy('created_at', 'asc');
if ($messages->count() > 0) {
$description .= PHP_EOL . PHP_EOL . 'Customer Chat: ' . PHP_EOL;
foreach ($messages->get() as $message) {
$description .= $message->participant->name . ': ' . $message->message . PHP_EOL;
}
}
if ($activity->summary) {
$description .= PHP_EOL . PHP_EOL . 'Summary: ' . PHP_EOL . $activity->summary;
}
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$description = $activity->description;
break;
}
return $description;
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
// This is the user provided activity subject field.
if (empty($fields['name'])) {
return null;
}
$due = null;
if (empty($fields['due_date']) === false) {
$formatDue = Carbon::parse($fields['due_date']);
$due = $formatDue->toDateTimeString();
}
$clTask = $this->client->post('task', [
'_type' => 'lead',
'lead_id' => $activity->account->crm_provider_id,
'assigned_to' => $this->profile->crm_provider_id,
'text' => $fields['name'],
'date' => $due,
'is_complete' => false,
]);
// We don't actually create a corresponding activity object on our side yet.
return $clTask['id'];
}
/**
* Store transcripts as note.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
if ($activity->account_id === null) {
// We can only log to accounts (leads).
return;
}
// Generate activity transcription.
$transcriptionData = $this->generateTranscription($activity);
// Truncate Notes with max notes length because transcription text could be very long.
$transcripts = mb_strimwidth($transcriptionData, 0, self::NOTE_BODY_MAX_LENGTH);
$clActivity = $this->client->post('activity/note', [
'lead_id' => $activity->account_id ? $activity->account->crm_provider_id : null,
'created_by' => $this->profile->crm_provider_id,
'user_id' => $this->profile->crm_provider_id,
'note' => $transcripts,
]);
// Store CRM Activity ID in transcription.
$transcription = $activity->getTranscription();
$transcription->crm_activity_id = $clActivity['id'];
$transcription->save();
}
public function parseObjectType(string $objectId): string
{
if (Str::startsWith($objectId, 'lead')) {
return 'account';
}
if (Str::startsWith($objectId, 'cont')) {
return 'contact';
}
if (Str::startsWith($objectId, 'oppo')) {
return 'opportunity';
}
throw new InvalidArgumentException('Unsupported Object Type');
}
/**
* @inheritdoc
*/
public function updateStage($crmObject, Stage $stage): void
{
if ($crmObject instanceof Lead) {
// This would never get invoked since we merge lead/accounts in Close.
$this->client->put('lead/' . $crmObject->crm_provider_id, [
'status' => $stage->crm_provider_id,
]);
} else {
$this->client->put('opportunity/' . $crmObject->crm_provider_id, [
'status_id' => $stage->crm_provider_id,
]);
}
}
public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void
{
$this->getClient()->updateObject($this->convertObjectTypeToResource($objectType), $objectId, $data);
}
public function prepareValueForUpdate(array $params): array
{
$convertedValue = $this->fieldValueConverter->convertToCrm(
$this->config,
$params['fieldName'],
$params['fieldValue'],
);
if ($this->isCustomField($params['fieldName'])) {
$params['fieldName'] = 'custom.' . $params['fieldName'];
}
$params['fieldValue'] = $convertedValue;
return parent::prepareValueForUpdate($params);
}
public function getRecord(string $objectType, string $objectId, array $fields = []): array
{
return $this->client->get($this->convertObjectTypeToResource($objectType) . '/' . $objectId);
}
/**
*
* @throws UnexpectedValueException
*/
private function convertObjectTypeToResource(string $objectType): string
{
switch ($objectType) {
case FieldData::OBJECT_OPPORTUNITY:
return 'opportunity';
case FieldData::OBJECT_CONTACT:
return 'contact';
case FieldData::OBJECT_ACCOUNT:
return 'lead';
case FieldData::OBJECT_TASK:
return 'activity';
default:
throw new UnexpectedValueException('Unsupported object type "' . $objectType . '"');
}
}
public function generateProviderUrl(string $providerId, string $objectType): ?string
{
$baseUrl = 'https://app.close.com/';
$url = null;
switch ($objectType) {
case 'account':
$url = $baseUrl . 'lead/' . $providerId;
break;
case 'contact':
$contact = $this->config->contacts()->where('crm_provider_id', $providerId)->first();
if ($contact && $contact->account_id) {
$url = $baseUrl . 'lead/' . $contact->account->crm_provider_id;
}
break;
default:
// Sadly we can't deeplink to anything else in Close UI.
$url = null;
}
return $url;
}
/**
* Generate transcription for the activity.
*/
private function generateTranscription(Activity $activity): string
{
if (! $this->config->store_transcript) {
// If sending transcription to activity toggle is disabled
return '';
}
return $this->transcriptionService
->findTranscriptionByActivity($activity)
->map(static function (array $transcriptionSegment): string {
return $transcriptionSegment['formattedStartsAt'] . ' | ' . $transcriptionSegment['transcript'];
})
->implode(PHP_EOL);
}
public function verifyTaskExists(Activity $activity): bool
{
$crmProviderId = $activity->getCrmProviderId();
$cacheKey = "crm_task_exists:{$this->config->getId()}:$crmProviderId";
return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {
try {
$client = $this->getClient();
$task = $client->get('task/' . $crmProviderId);
return ! empty($task);
} catch (HttpNotFoundException) {
// Task not found in CRM - this is expected and permanent
$this->logger->info('[Close] Task not found during verification', [
'task_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"39","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Close;\n\nuse Cache;\nuse Carbon\\Carbon;\nuse Exception;\nuse GuzzleHttp\\Exception\\ClientException;\nuse Illuminate\\Support\\Str;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\CloseInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\ServiceUnavailableException;\nuse Jiminny\\Exceptions\\UnexpectedCallException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Close\\Processor\\AccountProcessor;\nuse Jiminny\\Services\\Crm\\Close\\Processor\\MetadataProcessor;\nuse Jiminny\\Services\\Crm\\Close\\Processor\\OpportunityProcessor;\nuse Jiminny\\Services\\Crm\\Close\\Processor\\StageProcessor;\nuse Jiminny\\Services\\Crm\\Helpers\\FilterJoinedParticipants;\nuse Jiminny\\Services\\Crm\\Metadata\\OpportunityMetadata;\nuse Jiminny\\Services\\Crm\\Metadata\\ProfileMetadata;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Sentry;\nuse UnexpectedValueException;\n\nclass Service extends BaseService implements\n CloseInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n RemoteEntityManipulationInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n SupportsObjectTypeParseInterface,\n VerifyTaskExistsInterface\n{\n private const int NOTE_BODY_MAX_LENGTH = 3000000;\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n private StandardFieldMetadata $standardFieldMetadata;\n private MetadataProcessor $metadataProcessor;\n private FieldValueConverter $fieldValueConverter;\n private StageProcessor $stageProcessor;\n private OpportunityProcessor $opportunityProcessor;\n private AccountProcessor $accountProcessor;\n\n public function __construct(\n Client $client,\n StandardFieldMetadata $standardFieldMetadata,\n MetadataProcessor $metadataProcessor,\n FieldValueConverter $fieldValueConverter,\n StageProcessor $stageResolver,\n OpportunityProcessor $opportunityProcessor,\n AccountProcessor $accountProcessor,\n private readonly ProspectPhotoPathService $prospectPhotoPathService,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->standardFieldMetadata = $standardFieldMetadata;\n $this->metadataProcessor = $metadataProcessor;\n $this->fieldValueConverter = $fieldValueConverter;\n $this->stageProcessor = $stageResolver;\n $this->opportunityProcessor = $opportunityProcessor;\n $this->accountProcessor = $accountProcessor;\n }\n\n public function getDisplayName(): string\n {\n return 'Close';\n }\n\n public function setConfiguration(Configuration $config): void\n {\n parent::setConfiguration($config);\n\n $this->metadataProcessor->setConfiguration($config);\n $this->stageProcessor->setConfiguration($config);\n $this->opportunityProcessor->setConfiguration($config);\n $this->accountProcessor->setConfiguration($config);\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n return $user->getTeam()->getOwner()->getSocialAccount(SocialAccount::PROVIDER_CLOSE);\n }\n\n private function getClient(): Client\n {\n if (! $this->client instanceof Client) {\n throw new UnexpectedCallException('Client not set');\n }\n\n return $this->client;\n }\n\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return $this->fieldValueConverter->convertFromCrm($fieldType, $fieldValue);\n }\n\n protected function getFieldTypes(): array\n {\n return [\n parent::OBJECT_OPPORTUNITY,\n parent::OBJECT_CONTACT,\n parent::OBJECT_ACCOUNT,\n ];\n }\n\n protected function getFields(string $crmObject): array\n {\n // not used\n return [];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n // Set up the activity field as the default Type.\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'type',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n public function syncFields(): void\n {\n $this->syncStandardFields();\n $this->syncCustomFields();\n }\n\n /**\n * @important Works only for custom fields\n */\n public function syncField(Field $field): void\n {\n $resource = $this->convertObjectTypeToResource($field->getObjectType());\n\n // We can only sync custom fields in this CRM.\n if ($this->isCustomField($field->getCrmProviderId()) === false) {\n return;\n }\n\n $crmField = $this->getClient()->fetchCustomFieldDefinition($resource, $field->getCrmProviderId());\n\n $this->metadataProcessor->syncField($crmField);\n }\n\n private function isCustomField(string $fieldId): bool\n {\n return strpos($fieldId, 'cf_') === 0;\n }\n\n /**\n * @inheritdoc\n */\n public function importPicklistValues(Field $field): array\n {\n // handled in syncFields()\n return [];\n }\n\n /**\n * @important We only support stages on the opportunity object\n *\n * @param string[]|null $types\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n if (! $missingStageName) {\n // This is taken care of by syncOrganization()\n return null;\n }\n\n $stage = $this->stageProcessor->resolveFromStageId($missingStageName);\n\n if ($stage instanceof Stage) {\n return $stage;\n }\n\n $stageMetadata = $this->getClient()->fetchStage($missingStageName);\n\n if (! $stageMetadata) {\n $this->logger->error('Stage does not exist', [\n 'stage' => $missingStageName,\n ]);\n\n return null;\n }\n\n\n return $this->stageProcessor->importStage($stageMetadata);\n }\n\n /**\n * @inheritdoc\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Even though Close.io has the concept of \"leads\", they fit more into our concept of accounts.\n return 0;\n }\n\n /**\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Not a supported entity.\n return null;\n }\n\n /**\n * @throws Exception\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n foreach ($this->getClient()->listAccounts($since) as $clAccount) {\n // Only sync if previously imported.\n if ($this->hasAccount($clAccount->getId())) {\n $this->importAccount($clAccount);\n $syncCount++;\n }\n }\n } catch (Exception $exception) {\n $this->logger->error('Account sync failed', [\n 'error' => $exception->getMessage(),\n ]);\n\n throw $exception;\n }\n\n return $syncCount;\n }\n\n public function syncAccount(string $crmId): ?Account\n {\n return $this->accountProcessor->syncAccount($crmId);\n }\n\n private function importAccount($crmData): Account\n {\n return $this->accountProcessor->importAccountMetadata($crmData);\n }\n\n /**\n * @throws CloseException\n */\n public function syncOpportunities(array $parameters, ?string $strategy = null): int\n {\n $strategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $strategies = $strategyResolver->getStrategies($this->config, $strategy);\n\n $syncCount = 0;\n\n try {\n $opportunities = [];\n foreach ($strategies as $syncStrategy) {\n $opportunitiesData = $syncStrategy->fetchOpportunities($parameters);\n $opportunities[] = $opportunitiesData['data'];\n\n if ($opportunitiesData['has_more']) {\n $this->logger->info('[Close] Sync Opportunities - count warning', [\n 'team_id' => $this->config->getTeam()->getId(),\n 'total' => $opportunitiesData['total'],\n 'count' => $opportunitiesData['count'],\n 'skip' => $opportunitiesData['skip'],\n 'strategies_count' => count($strategies),\n ]);\n }\n }\n\n $opportunities = array_merge(...$opportunities);\n } catch (CrmException $exception) {\n $this->logger->error('Fetching opportunity data failed', [\n 'team' => $this->getTeam()->getSlug(),\n 'error' => $exception->getMessage(),\n ]);\n\n return 0;\n }\n\n foreach ($opportunities as $opportunityMetadata) {\n try {\n $this->importOpportunity($opportunityMetadata);\n $syncCount++;\n } catch (Exception $exception) {\n $this->logger->warning('Opportunity sync failed', [\n 'opportunity' => $opportunityMetadata->getId(),\n 'error' => $exception->getMessage(),\n ]);\n }\n }\n\n return $syncCount;\n }\n\n public function syncOpportunity(string $crmId): ?Opportunity\n {\n $strategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n\n $strategy = $strategyResolver->resolve(\n $this->config,\n OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY,\n );\n\n $parameters = ['crm_id' => $crmId];\n\n $opportunity = $strategy->fetchOpportunities($parameters);\n\n if (empty($opportunity['data'])) {\n return null;\n }\n\n return $this->importOpportunity($opportunity['data']);\n }\n\n private function importOpportunity(OpportunityMetadata $crmData): ?Opportunity\n {\n if (! $crmData->getLeadId()) {\n $this->logger->warning('Opportunity does not have a lead ID', [\n 'opportunity' => $crmData->getId(),\n ]);\n\n return null;\n }\n\n $account = $this->getConfiguration()\n ->accounts()\n ->where('crm_provider_id', $crmData->getLeadId())\n ->first();\n\n if ($account === null) {\n $account = $this->accountProcessor->syncAccount($crmData->getLeadId());\n }\n\n /** @var Profile $profile */\n $profile = $this->getConfiguration()\n ->profiles()\n ->where('crm_provider_id', $crmData->getUserId())\n ->first();\n\n $userId = $profile?->getUserId() ?? $account?->getUserId();\n if ($userId === null) {\n $this->logger->error('[Close] | Skip import, no user_id found', [\n 'id' => $crmData->getId(),\n ]);\n\n return null;\n }\n\n $stage = $this->getConfiguration()\n ->stages()\n ->where('crm_provider_id', $crmData->getStageId())\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $crmData->getStageId());\n }\n\n return $this->opportunityProcessor->import($crmData, $account, $stage, $profile);\n }\n\n /**\n * @param array<string,string> $crmData\n * @param string[] $crmFields\n */\n public function importOpportunityCrmFieldData(array $crmData, array $crmFields, int $opportunityId): void\n {\n // handled in importOpportunity\n }\n\n /**\n * @inheritdoc\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n /** No way to sync today.\n $clContacts = $this->client->get('lead', [\n 'date_updated__gte' => $since->toDateString(),\n '_order_by' => '-date_updated',\n ]);\n\n foreach ($clContacts as $clContact) {\n // Only sync if previously imported.\n if ($this->hasContact($clContact['id'])) {\n $this->importContact($clContact);\n $syncCount++;\n }\n }\n **/\n } catch (Exception $exception) {\n // Do nothing for now.\n throw $exception;\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n $clContact = $this->client->get('contact/' . $crmId);\n } catch (HttpNotFoundException $exception) {\n return null;\n }\n\n return $this->importContact($clContact);\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData): Contact\n {\n $account = null;\n if ($crmData['lead_id']) {\n $account = $this->team\n ->accounts()\n ->where('crm_provider_id', $crmData['lead_id'])\n ->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmData['lead_id']);\n }\n }\n\n $mobilePhone = $parsedNumber = null;\n foreach ($crmData['phones'] as $phoneNumber) {\n if ($phoneNumber['type'] === 'mobile') {\n $mobilePhone = $this->buildContactMobilePhone(null, $phoneNumber['phone']);\n } else {\n $parsedNumber = $this->buildContactPhone(null, $phoneNumber['phone']);\n }\n }\n\n $email = null;\n if (empty($crmData['emails']) === false) {\n $email = mb_strimwidth($crmData['emails'][0]['email'], 0, 80);\n }\n\n $profile = $this->config->profiles()->where('crm_provider_id', (string) $crmData['updated_by'])->first();\n\n $data = [\n 'account_id' => $account->id ?? null,\n 'team_id' => $this->team->id,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['updated_by'],\n 'name' => $crmData['name'] ?? 'Unknown',\n 'email' => $email,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobilePhone ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'title' => mb_strimwidth($crmData['title'] ?? '', 0, 128),\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['id'],\n modelType: Contact::class,\n fileName: $crmData['id'],\n avatarText: $crmData['name'] ?? 'Unknown'\n ),\n 'remotely_created_at' => Carbon::parse($crmData['date_created']),\n ];\n\n /** @var Contact */\n return $this->config->contacts()->updateOrCreate(['crm_provider_id' => (string) $crmData['id']], $data);\n }\n\n private function buildContactPhone(?string $countryCode, ?string $number): ?array\n {\n if ($number) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($number, 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n return $parsedNumber;\n }\n\n private function buildContactMobilePhone(?string $countryCode, ?string $number): ?string\n {\n return $number ? mb_strimwidth(phone_e164($countryCode, $number), 0, 25) : null;\n }\n\n public function syncOrganization(): void\n {\n $organisation = $this->getClient()->fetchOrganisation();\n\n $this->metadataProcessor->syncOrganisation($organisation);\n\n foreach ($organisation->getPipelines() as $pipelineMetadata) {\n $this->metadataProcessor->syncPipeline($pipelineMetadata);\n }\n }\n\n private function syncStandardFields(): void\n {\n // Currently we sync only opportunity fields\n $stages = $this->getClient()->listStages();\n foreach ($this->standardFieldMetadata->opportunity($stages) as $fieldMetadata) {\n $this->metadataProcessor->syncField($fieldMetadata);\n }\n\n $this->config->save();\n }\n\n private function syncCustomFields(): void\n {\n foreach ($this->getFieldTypes() as $fieldType) {\n $objectType = $this->convertObjectTypeToResource($fieldType);\n $currentFields = $this->getClient()->listCustomFieldDefinitions($objectType);\n\n foreach ($currentFields as $fieldMetadata) {\n $this->metadataProcessor->syncField($fieldMetadata);\n }\n }\n\n $this->config->save();\n }\n\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n /*\n * Fetch the profile of the user from the database\n * Then fetch the user metadata from Close and update it\n * In case there's no profile for the user, proceed with syncing all users\n */\n $foundUser = null;\n\n if ($userToSearch) {\n $profile = $userToSearch->getProfile();\n\n if ($profile instanceof Profile) {\n $crmProviderId = $profile->getCrmProviderId();\n\n if ($crmProviderId) {\n $profileMetadata = $this->getClient()->fetchUser($crmProviderId);\n\n if (! $profileMetadata instanceof ProfileMetadata) {\n return null;\n }\n\n return $this->metadataProcessor->syncProfile($profileMetadata);\n }\n }\n }\n\n foreach ($this->getClient()->listUsers() as $userMetadata) {\n $userProfile = $this->metadataProcessor->syncProfile($userMetadata);\n\n if (\n $userToSearch instanceof User\n && $userProfile instanceof Profile\n && $userProfile->getUserId() === $userToSearch->getId()\n ) {\n $foundUser = $userProfile;\n }\n }\n\n return $foundUser;\n }\n\n public function syncProfileFields(): void\n {\n // Not used.\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n $data = Cache::remember($this->team->id . $name . $count . $offset, 300, function () use ($name, $count, $offset) {\n $data = [];\n\n try {\n // If search phrase resembles phone number remove special symbols\n if (preg_match('/^([0-9\\s\\-\\+\\(\\)]*)$/', $name)) {\n $name = '+' . preg_replace('/[\\s\\-\\+\\(\\)]/', '', $name);\n }\n\n // Close do not provide a unified way to search, so we must hack our own.\n $objects = $this->client->get('lead', [\n 'query' => 'sort:date_updated name:(' . $name . ') or email:(' . $name . ') or phone:(' . $name . ')',\n '_limit' => $count, '_skip' => $offset,\n ]);\n } catch (\\GuzzleHttp\\Exception\\ServerException $exception) {\n throw new ServiceUnavailableException($exception->getMessage());\n }\n\n foreach ($objects['data'] as $object) {\n // We need a contact to dial it.\n if (empty($object['contacts'])) {\n continue;\n }\n\n foreach ($object['contacts'] as $contact) {\n $record = [\n 'crmId' => $contact['id'],\n 'crmUrl' => $this->generateProviderUrl($object['id'], 'account'),\n 'name' => $contact['name'],\n 'industry' => null,\n 'title' => $contact['title'],\n 'organization' => $object['display_name'],\n 'prospectType' => 'contact',\n 'phoneNumbers' => [],\n ];\n\n foreach ($contact['phones'] as $phone) {\n if ($phone['type'] === 'mobile') {\n $number = $this->buildContactMobilePhone(null, $phone['phone']);\n\n $record['phoneNumbers'][] = [\n 'number' => $number,\n 'nationalFormat' => phone_national(null, $number),\n 'type' => 'mobile',\n ];\n } else {\n $parsedNumber = $this->buildContactPhone(null, $phone['phone']);\n\n // Add phone number to record.\n if (empty($parsedNumber['phone']) === false) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national(null, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n }\n }\n\n $data[] = $record;\n }\n }\n\n return $data;\n });\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n $contact = null;\n $account = null;\n\n if ($crmAccountId) {\n $account = $this->config->accounts()->where('crm_provider_id', $crmAccountId)->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmAccountId);\n }\n }\n\n if ($crmContactId) {\n $contact = $this->config->contacts()->where('crm_provider_id', $crmContactId)->first();\n\n if ($contact === null) {\n $contact = $this->syncContact($crmContactId);\n }\n }\n\n if ($contact || $account) {\n if ($contact && $account === null) {\n $account = $contact->account;\n }\n\n if ($account === null) {\n return [];\n }\n\n $params = [\n 'lead_id' => $account->crm_provider_id,\n '_order_by' => '-date_updated',\n ];\n\n $onlyOpen = true;\n switch ($this->config->opportunity_assignment_rule) {\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:\n $params['_order_by'] = '-date_updated';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:\n $params['_order_by'] = '-date_created';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:\n $params['_order_by'] = 'date_created';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:\n $params['_order_by'] = '-date_updated';\n $onlyOpen = false;\n }\n\n if ($onlyOpen) {\n $params['status_type__in'] = 'active,won';\n }\n\n $clOpportunities = $this->client->get('opportunity', $params);\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n foreach ($clOpportunities['data'] as $clOpportunity) {\n $stage = $this->config\n ->stages()\n ->where('crm_provider_id', $clOpportunity['status_id'])\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $clOpportunity['status_id']);\n }\n\n $record = [\n 'crmId' => $clOpportunity['id'],\n 'name' => $clOpportunity['note'],\n 'value' => currency_format($clOpportunity['value'], $clOpportunity['value_currency']),\n 'won' => $stage->probability === 100.00,\n 'closed' => $clOpportunity['status_type'] !== 'active',\n 'stage' => [\n 'id' => $stage->id_string,\n 'name' => $stage->name,\n ],\n 'recordType' => [],\n ];\n\n if ($ownerId && isset($clOpportunity['user_id']) && $clOpportunity['user_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n $crmId = null;\n\n if ($objectType === 'contact') {\n $contact = $this->syncContact($objectId);\n\n if ($contact && $contact->account_id) {\n $crmId = $contact->account->crm_provider_id;\n }\n } else {\n $crmId = $objectId;\n }\n\n if ($crmId) {\n $clTasks = $this->client->get('task', [\n 'lead_id' => $crmId,\n '_type' => 'lead',\n 'assigned_to' => $this->profile->crm_provider_id,\n 'is_complete' => 'false',\n '_order_by' => 'date',\n ]);\n\n foreach ($clTasks['data'] as $clTask) {\n $data[] = [\n 'crmId' => $clTask['id'],\n 'subject' => $clTask['text'],\n 'due' => $clTask['date'] ?? null,\n 'type' => null,\n ];\n }\n }\n\n return $data;\n }\n\n /**\n * Try to find email address in CRM service\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $clObject = $this->client->get('lead', [\n 'query' => 'sort:updated contact(email(email:\"' . $email . '\"))',\n '_limit' => 1,\n ]);\n\n if ($clObject['total_results'] > 0) {\n foreach ($clObject['data'][0]['contacts'] as $clContact) {\n foreach ($clContact['emails'] as $clEmail) {\n if ($email === $clEmail['email']) {\n $data = $this->convertCrmData($clContact['id'], $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n }\n }\n }\n\n return null;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n // Check if the user is internal.\n $teamMember = $this->team->users()->where('phone', $phone)->exists();\n\n // Skip the attendee if internal.\n if ($teamMember === false) {\n $clObject = $this->client->get('lead', [\n 'query' => 'sort:updated contact(' . $phone . ')',\n '_limit' => 1,\n ]);\n\n if ($clObject['total_results'] > 0) {\n foreach ($clObject['data'][0]['contacts'] as $clContact) {\n foreach ($clContact['phones'] as $clPhone) {\n if ($phone === $clPhone['phone']) {\n $data = $this->convertCrmData($clContact['id'], $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n }\n }\n }\n }\n\n return null;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $clObject = $this->client->get('lead', [\n 'query' => 'sort:updated contact(name:\"' . $name . '\")',\n '_limit' => 1,\n ]);\n\n if ($clObject['total_results'] > 0) {\n foreach ($clObject['data'][0]['contacts'] as $clContact) {\n if ($clContact['name'] === $name || $clContact['display_name'] === $name) {\n $data = $this->convertCrmData($clContact['id'], $userId);\n\n return ! empty(array_filter($data)) ? $data : false;\n }\n }\n }\n\n return false;\n });\n\n return is_array($result) ? $result : null;\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n private function convertCrmData(string $crmId, ?int $userId = null): array\n {\n $lead = null;\n $opportunity = null;\n $account = null;\n $stage = null;\n $countryCode = null;\n\n $contact = $this->syncContact($crmId);\n if ($contact) {\n $account = $contact->account;\n\n if ($contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account) {\n $countryCode = $account->country_code;\n }\n\n try {\n $cpOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId,\n );\n\n if (! empty($cpOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($cpOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception) {\n // Nothing to see here.\n }\n }\n\n return [\n $lead,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->type) {\n case Activity::TYPE_CONFERENCE:\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildCallPayload($activity);\n\n break;\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n private function mapStatus(string $status): string\n {\n switch ($status) {\n case Activity::STATUS_COMPLETED:\n case Activity::STATUS_IN_PROGRESS:\n case Activity::STATUS_FAILED:\n case Activity::STATUS_NO_ANSWER:\n case Activity::STATUS_BUSY:\n default:\n return $status;\n case Activity::STATUS_CANCELLED:\n return 'cancel';\n }\n }\n\n /**\n * @throws CrmException\n */\n private function buildCallPayload(Activity $activity): Activity\n {\n try {\n if ($activity->crm_provider_id) {\n // The activity should be logged under the existing Task (not Activity).\n $data = [\n '_type' => 'lead',\n 'lead_id' => $activity->account->crm_provider_id,\n 'assigned_to' => $this->profile->crm_provider_id,\n 'text' => $this->generateActivityDescription($activity),\n 'date' => $activity->getActualEndTime()->toDateString(),\n 'is_complete' => true,\n ];\n\n $this->logger->info('[Close CRM] Updating task', [\n 'activity' => $activity->id,\n 'crm_id' => $activity->crm_provider_id,\n 'data' => $data,\n ]);\n\n $this->client->put('task/' . $activity->crm_provider_id, $data);\n } else {\n // Just create an activity.\n $data = [\n 'lead_id' => $activity->account_id ? $activity->account->crm_provider_id : null,\n 'contact_id' => $activity->contact_id ? $activity->contact->crm_provider_id : null,\n 'created_by' => $this->profile->crm_provider_id,\n 'user_id' => $this->profile->crm_provider_id,\n 'direction' => $activity->getCrmType() === Activity::TYPE_SOFTPHONE ? 'outbound' : 'inbound',\n 'status' => $this->mapStatus($activity->getStatus()),\n 'note' => $this->generateActivityDescription($activity),\n 'duration' => $this->mapStatus($activity->getStatus()) !== 'cancel' ? $activity->duration : 0,\n 'phone' => $activity->to ? $activity->to->phone_number : null,\n ];\n\n $clActivity = $this->client->post('activity/call', $data);\n\n $this->logger->info('[Close CRM] Creating activity', [\n 'activity' => $activity->id,\n 'crm_id' => $clActivity['id'],\n 'data' => $data,\n 'response' => $clActivity,\n ]);\n\n $activity->crm_provider_id = $clActivity['id'];\n $activity->save();\n }\n } catch (ClientException $exception) {\n $response = $exception->getResponse();\n\n if ($response === null) {\n // Trying to debug weird cases where this is null.\n Sentry::captureException($exception);\n }\n\n $responseBody = $response->getBody();\n $message = $responseBody;\n $errorCode = $response->getStatusCode();\n\n $jsonResponse = json_decode($responseBody, true);\n if (isset($jsonResponse[0]['message'])) {\n $message = $jsonResponse[0]['message'];\n }\n\n throw new CrmException($message, $errorCode);\n }\n\n return $activity;\n }\n\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $clActivity = $this->client->post('activity/sms', [\n 'lead_id' => $activity->account_id ? $activity->account->crm_provider_id : null,\n 'contact_id' => $activity->contact_id ? $activity->contact->crm_provider_id : null,\n 'created_by' => $this->profile->crm_provider_id,\n 'user_id' => $this->profile->crm_provider_id,\n 'status' => $activity->getType() === Activity::TYPE_SMS_OUTBOUND ? 'sent' : 'inbox',\n 'text' => $this->generateActivityDescription($activity),\n 'remote_phone' => $activity->to ? $activity->to->phone_number : null,\n 'local_phone' => $activity->to ? $activity->to->phone_number : null,\n 'source' => 'Close.io',\n ]);\n\n $activity->crm_provider_id = $clActivity['id'];\n $activity->save();\n\n return $activity;\n }\n\n private function generateActivityDescription(Activity $activity): string\n {\n $description = '';\n\n switch ($activity->type) {\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n case Activity::TYPE_CONFERENCE:\n if ($activity->hasActivityType()) {\n $description = $activity->getActivityType()->getName() . PHP_EOL . PHP_EOL;\n }\n if ($activity->hasTitle()) {\n $description .= $activity->getTitle() . PHP_EOL;\n }\n\n if ($activity->hasReasonCodeBotKicked()) {\n $description .= 'Notetaker removed from this meeting' . PHP_EOL . PHP_EOL;\n // When we fix the state to be Activity::RECORDING_RECORDED as it should be this can change.\n } elseif ($activity->hasReasonCodeNotCompliant()) {\n $description .= 'Notetaker did not join due to recording consent not being provided by attendees' . PHP_EOL . PHP_EOL;\n } elseif ($activity->canReviewActivity()) {\n $playbackUrl = $activity->user->team->partner->getPlaybackUrl($activity);\n $description .= 'Review Activity:' . PHP_EOL . $playbackUrl . PHP_EOL . PHP_EOL;\n }\n\n if ($activity->type === Activity::TYPE_CONFERENCE) {\n $description .= 'Attendees:'\n . PHP_EOL\n . (new FilterJoinedParticipants())->toString($activity);\n }\n\n if (\\count($activity->notes) > 0) {\n $description .= PHP_EOL . PHP_EOL . 'Notes: ' . PHP_EOL;\n\n foreach ($activity->notes as $note) {\n $time = ($note->time > 3600) ? gmdate('H:i:s', $note->time) : gmdate('i:s', $note->time);\n $description .= $time . ' ' . $note->note . PHP_EOL;\n }\n }\n\n // Get all private messages.\n $messages = $activity->messages()\n ->where('is_private', 1)\n ->orderBy('created_at', 'asc');\n\n if ($messages->count() > 0) {\n $description .= PHP_EOL . PHP_EOL . 'Coaching Chat: ' . PHP_EOL;\n\n foreach ($messages->get() as $message) {\n $description .= $message->participant->name . ': ' . $message->message . PHP_EOL;\n }\n }\n\n // Get all public messages.\n $messages = $activity->messages()\n ->where('is_private', 0)\n ->orderBy('created_at', 'asc');\n\n if ($messages->count() > 0) {\n $description .= PHP_EOL . PHP_EOL . 'Customer Chat: ' . PHP_EOL;\n\n foreach ($messages->get() as $message) {\n $description .= $message->participant->name . ': ' . $message->message . PHP_EOL;\n }\n }\n\n if ($activity->summary) {\n $description .= PHP_EOL . PHP_EOL . 'Summary: ' . PHP_EOL . $activity->summary;\n }\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $description = $activity->description;\n\n break;\n }\n\n return $description;\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n // This is the user provided activity subject field.\n if (empty($fields['name'])) {\n return null;\n }\n\n $due = null;\n if (empty($fields['due_date']) === false) {\n $formatDue = Carbon::parse($fields['due_date']);\n $due = $formatDue->toDateTimeString();\n }\n\n $clTask = $this->client->post('task', [\n '_type' => 'lead',\n 'lead_id' => $activity->account->crm_provider_id,\n 'assigned_to' => $this->profile->crm_provider_id,\n 'text' => $fields['name'],\n 'date' => $due,\n 'is_complete' => false,\n ]);\n\n // We don't actually create a corresponding activity object on our side yet.\n return $clTask['id'];\n }\n\n /**\n * Store transcripts as note.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n if ($activity->account_id === null) {\n // We can only log to accounts (leads).\n return;\n }\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, self::NOTE_BODY_MAX_LENGTH);\n\n $clActivity = $this->client->post('activity/note', [\n 'lead_id' => $activity->account_id ? $activity->account->crm_provider_id : null,\n 'created_by' => $this->profile->crm_provider_id,\n 'user_id' => $this->profile->crm_provider_id,\n 'note' => $transcripts,\n ]);\n\n // Store CRM Activity ID in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $clActivity['id'];\n $transcription->save();\n }\n\n public function parseObjectType(string $objectId): string\n {\n if (Str::startsWith($objectId, 'lead')) {\n return 'account';\n }\n\n if (Str::startsWith($objectId, 'cont')) {\n return 'contact';\n }\n\n if (Str::startsWith($objectId, 'oppo')) {\n return 'opportunity';\n }\n\n throw new InvalidArgumentException('Unsupported Object Type');\n }\n\n /**\n * @inheritdoc\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n if ($crmObject instanceof Lead) {\n // This would never get invoked since we merge lead/accounts in Close.\n $this->client->put('lead/' . $crmObject->crm_provider_id, [\n 'status' => $stage->crm_provider_id,\n ]);\n } else {\n $this->client->put('opportunity/' . $crmObject->crm_provider_id, [\n 'status_id' => $stage->crm_provider_id,\n ]);\n }\n }\n\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $this->getClient()->updateObject($this->convertObjectTypeToResource($objectType), $objectId, $data);\n }\n\n public function prepareValueForUpdate(array $params): array\n {\n $convertedValue = $this->fieldValueConverter->convertToCrm(\n $this->config,\n $params['fieldName'],\n $params['fieldValue'],\n );\n\n if ($this->isCustomField($params['fieldName'])) {\n $params['fieldName'] = 'custom.' . $params['fieldName'];\n }\n\n $params['fieldValue'] = $convertedValue;\n\n return parent::prepareValueForUpdate($params);\n }\n\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n return $this->client->get($this->convertObjectTypeToResource($objectType) . '/' . $objectId);\n }\n\n /**\n *\n * @throws UnexpectedValueException\n */\n private function convertObjectTypeToResource(string $objectType): string\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return 'opportunity';\n\n case FieldData::OBJECT_CONTACT:\n return 'contact';\n\n case FieldData::OBJECT_ACCOUNT:\n return 'lead';\n\n case FieldData::OBJECT_TASK:\n return 'activity';\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $baseUrl = 'https://app.close.com/';\n $url = null;\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'lead/' . $providerId;\n\n break;\n\n case 'contact':\n $contact = $this->config->contacts()->where('crm_provider_id', $providerId)->first();\n if ($contact && $contact->account_id) {\n $url = $baseUrl . 'lead/' . $contact->account->crm_provider_id;\n }\n\n break;\n\n default:\n // Sadly we can't deeplink to anything else in Close UI.\n $url = null;\n }\n\n return $url;\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n return $this->transcriptionService\n ->findTranscriptionByActivity($activity)\n ->map(static function (array $transcriptionSegment): string {\n return $transcriptionSegment['formattedStartsAt'] . ' | ' . $transcriptionSegment['transcript'];\n })\n ->implode(PHP_EOL);\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $client = $this->getClient();\n $task = $client->get('task/' . $crmProviderId);\n\n return ! empty($task);\n } catch (HttpNotFoundException) {\n // Task not found in CRM - this is expected and permanent\n $this->logger->info('[Close] Task not found during verification', [\n 'task_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n } catch (CloseException $e) {\n // Handle 404 responses from Close API\n if ($e->getResponseStatusCode() === 404) {\n $this->logger->info('[Close] Task not found during verification (404)', [\n 'task_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n\n // Re-throw other Close exceptions for retry\n throw $e;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Close;\n\nuse Cache;\nuse Carbon\\Carbon;\nuse Exception;\nuse GuzzleHttp\\Exception\\ClientException;\nuse Illuminate\\Support\\Str;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\CloseInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\ServiceUnavailableException;\nuse Jiminny\\Exceptions\\UnexpectedCallException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Close\\Processor\\AccountProcessor;\nuse Jiminny\\Services\\Crm\\Close\\Processor\\MetadataProcessor;\nuse Jiminny\\Services\\Crm\\Close\\Processor\\OpportunityProcessor;\nuse Jiminny\\Services\\Crm\\Close\\Processor\\StageProcessor;\nuse Jiminny\\Services\\Crm\\Helpers\\FilterJoinedParticipants;\nuse Jiminny\\Services\\Crm\\Metadata\\OpportunityMetadata;\nuse Jiminny\\Services\\Crm\\Metadata\\ProfileMetadata;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Sentry;\nuse UnexpectedValueException;\n\nclass Service extends BaseService implements\n CloseInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n RemoteEntityManipulationInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n SupportsObjectTypeParseInterface,\n VerifyTaskExistsInterface\n{\n private const int NOTE_BODY_MAX_LENGTH = 3000000;\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n private StandardFieldMetadata $standardFieldMetadata;\n private MetadataProcessor $metadataProcessor;\n private FieldValueConverter $fieldValueConverter;\n private StageProcessor $stageProcessor;\n private OpportunityProcessor $opportunityProcessor;\n private AccountProcessor $accountProcessor;\n\n public function __construct(\n Client $client,\n StandardFieldMetadata $standardFieldMetadata,\n MetadataProcessor $metadataProcessor,\n FieldValueConverter $fieldValueConverter,\n StageProcessor $stageResolver,\n OpportunityProcessor $opportunityProcessor,\n AccountProcessor $accountProcessor,\n private readonly ProspectPhotoPathService $prospectPhotoPathService,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->standardFieldMetadata = $standardFieldMetadata;\n $this->metadataProcessor = $metadataProcessor;\n $this->fieldValueConverter = $fieldValueConverter;\n $this->stageProcessor = $stageResolver;\n $this->opportunityProcessor = $opportunityProcessor;\n $this->accountProcessor = $accountProcessor;\n }\n\n public function getDisplayName(): string\n {\n return 'Close';\n }\n\n public function setConfiguration(Configuration $config): void\n {\n parent::setConfiguration($config);\n\n $this->metadataProcessor->setConfiguration($config);\n $this->stageProcessor->setConfiguration($config);\n $this->opportunityProcessor->setConfiguration($config);\n $this->accountProcessor->setConfiguration($config);\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n return $user->getTeam()->getOwner()->getSocialAccount(SocialAccount::PROVIDER_CLOSE);\n }\n\n private function getClient(): Client\n {\n if (! $this->client instanceof Client) {\n throw new UnexpectedCallException('Client not set');\n }\n\n return $this->client;\n }\n\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return $this->fieldValueConverter->convertFromCrm($fieldType, $fieldValue);\n }\n\n protected function getFieldTypes(): array\n {\n return [\n parent::OBJECT_OPPORTUNITY,\n parent::OBJECT_CONTACT,\n parent::OBJECT_ACCOUNT,\n ];\n }\n\n protected function getFields(string $crmObject): array\n {\n // not used\n return [];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n // Set up the activity field as the default Type.\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'type',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n public function syncFields(): void\n {\n $this->syncStandardFields();\n $this->syncCustomFields();\n }\n\n /**\n * @important Works only for custom fields\n */\n public function syncField(Field $field): void\n {\n $resource = $this->convertObjectTypeToResource($field->getObjectType());\n\n // We can only sync custom fields in this CRM.\n if ($this->isCustomField($field->getCrmProviderId()) === false) {\n return;\n }\n\n $crmField = $this->getClient()->fetchCustomFieldDefinition($resource, $field->getCrmProviderId());\n\n $this->metadataProcessor->syncField($crmField);\n }\n\n private function isCustomField(string $fieldId): bool\n {\n return strpos($fieldId, 'cf_') === 0;\n }\n\n /**\n * @inheritdoc\n */\n public function importPicklistValues(Field $field): array\n {\n // handled in syncFields()\n return [];\n }\n\n /**\n * @important We only support stages on the opportunity object\n *\n * @param string[]|null $types\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n if (! $missingStageName) {\n // This is taken care of by syncOrganization()\n return null;\n }\n\n $stage = $this->stageProcessor->resolveFromStageId($missingStageName);\n\n if ($stage instanceof Stage) {\n return $stage;\n }\n\n $stageMetadata = $this->getClient()->fetchStage($missingStageName);\n\n if (! $stageMetadata) {\n $this->logger->error('Stage does not exist', [\n 'stage' => $missingStageName,\n ]);\n\n return null;\n }\n\n\n return $this->stageProcessor->importStage($stageMetadata);\n }\n\n /**\n * @inheritdoc\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Even though Close.io has the concept of \"leads\", they fit more into our concept of accounts.\n return 0;\n }\n\n /**\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Not a supported entity.\n return null;\n }\n\n /**\n * @throws Exception\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n foreach ($this->getClient()->listAccounts($since) as $clAccount) {\n // Only sync if previously imported.\n if ($this->hasAccount($clAccount->getId())) {\n $this->importAccount($clAccount);\n $syncCount++;\n }\n }\n } catch (Exception $exception) {\n $this->logger->error('Account sync failed', [\n 'error' => $exception->getMessage(),\n ]);\n\n throw $exception;\n }\n\n return $syncCount;\n }\n\n public function syncAccount(string $crmId): ?Account\n {\n return $this->accountProcessor->syncAccount($crmId);\n }\n\n private function importAccount($crmData): Account\n {\n return $this->accountProcessor->importAccountMetadata($crmData);\n }\n\n /**\n * @throws CloseException\n */\n public function syncOpportunities(array $parameters, ?string $strategy = null): int\n {\n $strategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $strategies = $strategyResolver->getStrategies($this->config, $strategy);\n\n $syncCount = 0;\n\n try {\n $opportunities = [];\n foreach ($strategies as $syncStrategy) {\n $opportunitiesData = $syncStrategy->fetchOpportunities($parameters);\n $opportunities[] = $opportunitiesData['data'];\n\n if ($opportunitiesData['has_more']) {\n $this->logger->info('[Close] Sync Opportunities - count warning', [\n 'team_id' => $this->config->getTeam()->getId(),\n 'total' => $opportunitiesData['total'],\n 'count' => $opportunitiesData['count'],\n 'skip' => $opportunitiesData['skip'],\n 'strategies_count' => count($strategies),\n ]);\n }\n }\n\n $opportunities = array_merge(...$opportunities);\n } catch (CrmException $exception) {\n $this->logger->error('Fetching opportunity data failed', [\n 'team' => $this->getTeam()->getSlug(),\n 'error' => $exception->getMessage(),\n ]);\n\n return 0;\n }\n\n foreach ($opportunities as $opportunityMetadata) {\n try {\n $this->importOpportunity($opportunityMetadata);\n $syncCount++;\n } catch (Exception $exception) {\n $this->logger->warning('Opportunity sync failed', [\n 'opportunity' => $opportunityMetadata->getId(),\n 'error' => $exception->getMessage(),\n ]);\n }\n }\n\n return $syncCount;\n }\n\n public function syncOpportunity(string $crmId): ?Opportunity\n {\n $strategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n\n $strategy = $strategyResolver->resolve(\n $this->config,\n OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY,\n );\n\n $parameters = ['crm_id' => $crmId];\n\n $opportunity = $strategy->fetchOpportunities($parameters);\n\n if (empty($opportunity['data'])) {\n return null;\n }\n\n return $this->importOpportunity($opportunity['data']);\n }\n\n private function importOpportunity(OpportunityMetadata $crmData): ?Opportunity\n {\n if (! $crmData->getLeadId()) {\n $this->logger->warning('Opportunity does not have a lead ID', [\n 'opportunity' => $crmData->getId(),\n ]);\n\n return null;\n }\n\n $account = $this->getConfiguration()\n ->accounts()\n ->where('crm_provider_id', $crmData->getLeadId())\n ->first();\n\n if ($account === null) {\n $account = $this->accountProcessor->syncAccount($crmData->getLeadId());\n }\n\n /** @var Profile $profile */\n $profile = $this->getConfiguration()\n ->profiles()\n ->where('crm_provider_id', $crmData->getUserId())\n ->first();\n\n $userId = $profile?->getUserId() ?? $account?->getUserId();\n if ($userId === null) {\n $this->logger->error('[Close] | Skip import, no user_id found', [\n 'id' => $crmData->getId(),\n ]);\n\n return null;\n }\n\n $stage = $this->getConfiguration()\n ->stages()\n ->where('crm_provider_id', $crmData->getStageId())\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $crmData->getStageId());\n }\n\n return $this->opportunityProcessor->import($crmData, $account, $stage, $profile);\n }\n\n /**\n * @param array<string,string> $crmData\n * @param string[] $crmFields\n */\n public function importOpportunityCrmFieldData(array $crmData, array $crmFields, int $opportunityId): void\n {\n // handled in importOpportunity\n }\n\n /**\n * @inheritdoc\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n /** No way to sync today.\n $clContacts = $this->client->get('lead', [\n 'date_updated__gte' => $since->toDateString(),\n '_order_by' => '-date_updated',\n ]);\n\n foreach ($clContacts as $clContact) {\n // Only sync if previously imported.\n if ($this->hasContact($clContact['id'])) {\n $this->importContact($clContact);\n $syncCount++;\n }\n }\n **/\n } catch (Exception $exception) {\n // Do nothing for now.\n throw $exception;\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n $clContact = $this->client->get('contact/' . $crmId);\n } catch (HttpNotFoundException $exception) {\n return null;\n }\n\n return $this->importContact($clContact);\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData): Contact\n {\n $account = null;\n if ($crmData['lead_id']) {\n $account = $this->team\n ->accounts()\n ->where('crm_provider_id', $crmData['lead_id'])\n ->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmData['lead_id']);\n }\n }\n\n $mobilePhone = $parsedNumber = null;\n foreach ($crmData['phones'] as $phoneNumber) {\n if ($phoneNumber['type'] === 'mobile') {\n $mobilePhone = $this->buildContactMobilePhone(null, $phoneNumber['phone']);\n } else {\n $parsedNumber = $this->buildContactPhone(null, $phoneNumber['phone']);\n }\n }\n\n $email = null;\n if (empty($crmData['emails']) === false) {\n $email = mb_strimwidth($crmData['emails'][0]['email'], 0, 80);\n }\n\n $profile = $this->config->profiles()->where('crm_provider_id', (string) $crmData['updated_by'])->first();\n\n $data = [\n 'account_id' => $account->id ?? null,\n 'team_id' => $this->team->id,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['updated_by'],\n 'name' => $crmData['name'] ?? 'Unknown',\n 'email' => $email,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobilePhone ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'title' => mb_strimwidth($crmData['title'] ?? '', 0, 128),\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['id'],\n modelType: Contact::class,\n fileName: $crmData['id'],\n avatarText: $crmData['name'] ?? 'Unknown'\n ),\n 'remotely_created_at' => Carbon::parse($crmData['date_created']),\n ];\n\n /** @var Contact */\n return $this->config->contacts()->updateOrCreate(['crm_provider_id' => (string) $crmData['id']], $data);\n }\n\n private function buildContactPhone(?string $countryCode, ?string $number): ?array\n {\n if ($number) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($number, 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n return $parsedNumber;\n }\n\n private function buildContactMobilePhone(?string $countryCode, ?string $number): ?string\n {\n return $number ? mb_strimwidth(phone_e164($countryCode, $number), 0, 25) : null;\n }\n\n public function syncOrganization(): void\n {\n $organisation = $this->getClient()->fetchOrganisation();\n\n $this->metadataProcessor->syncOrganisation($organisation);\n\n foreach ($organisation->getPipelines() as $pipelineMetadata) {\n $this->metadataProcessor->syncPipeline($pipelineMetadata);\n }\n }\n\n private function syncStandardFields(): void\n {\n // Currently we sync only opportunity fields\n $stages = $this->getClient()->listStages();\n foreach ($this->standardFieldMetadata->opportunity($stages) as $fieldMetadata) {\n $this->metadataProcessor->syncField($fieldMetadata);\n }\n\n $this->config->save();\n }\n\n private function syncCustomFields(): void\n {\n foreach ($this->getFieldTypes() as $fieldType) {\n $objectType = $this->convertObjectTypeToResource($fieldType);\n $currentFields = $this->getClient()->listCustomFieldDefinitions($objectType);\n\n foreach ($currentFields as $fieldMetadata) {\n $this->metadataProcessor->syncField($fieldMetadata);\n }\n }\n\n $this->config->save();\n }\n\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n /*\n * Fetch the profile of the user from the database\n * Then fetch the user metadata from Close and update it\n * In case there's no profile for the user, proceed with syncing all users\n */\n $foundUser = null;\n\n if ($userToSearch) {\n $profile = $userToSearch->getProfile();\n\n if ($profile instanceof Profile) {\n $crmProviderId = $profile->getCrmProviderId();\n\n if ($crmProviderId) {\n $profileMetadata = $this->getClient()->fetchUser($crmProviderId);\n\n if (! $profileMetadata instanceof ProfileMetadata) {\n return null;\n }\n\n return $this->metadataProcessor->syncProfile($profileMetadata);\n }\n }\n }\n\n foreach ($this->getClient()->listUsers() as $userMetadata) {\n $userProfile = $this->metadataProcessor->syncProfile($userMetadata);\n\n if (\n $userToSearch instanceof User\n && $userProfile instanceof Profile\n && $userProfile->getUserId() === $userToSearch->getId()\n ) {\n $foundUser = $userProfile;\n }\n }\n\n return $foundUser;\n }\n\n public function syncProfileFields(): void\n {\n // Not used.\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n $data = Cache::remember($this->team->id . $name . $count . $offset, 300, function () use ($name, $count, $offset) {\n $data = [];\n\n try {\n // If search phrase resembles phone number remove special symbols\n if (preg_match('/^([0-9\\s\\-\\+\\(\\)]*)$/', $name)) {\n $name = '+' . preg_replace('/[\\s\\-\\+\\(\\)]/', '', $name);\n }\n\n // Close do not provide a unified way to search, so we must hack our own.\n $objects = $this->client->get('lead', [\n 'query' => 'sort:date_updated name:(' . $name . ') or email:(' . $name . ') or phone:(' . $name . ')',\n '_limit' => $count, '_skip' => $offset,\n ]);\n } catch (\\GuzzleHttp\\Exception\\ServerException $exception) {\n throw new ServiceUnavailableException($exception->getMessage());\n }\n\n foreach ($objects['data'] as $object) {\n // We need a contact to dial it.\n if (empty($object['contacts'])) {\n continue;\n }\n\n foreach ($object['contacts'] as $contact) {\n $record = [\n 'crmId' => $contact['id'],\n 'crmUrl' => $this->generateProviderUrl($object['id'], 'account'),\n 'name' => $contact['name'],\n 'industry' => null,\n 'title' => $contact['title'],\n 'organization' => $object['display_name'],\n 'prospectType' => 'contact',\n 'phoneNumbers' => [],\n ];\n\n foreach ($contact['phones'] as $phone) {\n if ($phone['type'] === 'mobile') {\n $number = $this->buildContactMobilePhone(null, $phone['phone']);\n\n $record['phoneNumbers'][] = [\n 'number' => $number,\n 'nationalFormat' => phone_national(null, $number),\n 'type' => 'mobile',\n ];\n } else {\n $parsedNumber = $this->buildContactPhone(null, $phone['phone']);\n\n // Add phone number to record.\n if (empty($parsedNumber['phone']) === false) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national(null, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n }\n }\n\n $data[] = $record;\n }\n }\n\n return $data;\n });\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n $contact = null;\n $account = null;\n\n if ($crmAccountId) {\n $account = $this->config->accounts()->where('crm_provider_id', $crmAccountId)->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmAccountId);\n }\n }\n\n if ($crmContactId) {\n $contact = $this->config->contacts()->where('crm_provider_id', $crmContactId)->first();\n\n if ($contact === null) {\n $contact = $this->syncContact($crmContactId);\n }\n }\n\n if ($contact || $account) {\n if ($contact && $account === null) {\n $account = $contact->account;\n }\n\n if ($account === null) {\n return [];\n }\n\n $params = [\n 'lead_id' => $account->crm_provider_id,\n '_order_by' => '-date_updated',\n ];\n\n $onlyOpen = true;\n switch ($this->config->opportunity_assignment_rule) {\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:\n $params['_order_by'] = '-date_updated';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:\n $params['_order_by'] = '-date_created';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:\n $params['_order_by'] = 'date_created';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:\n $params['_order_by'] = '-date_updated';\n $onlyOpen = false;\n }\n\n if ($onlyOpen) {\n $params['status_type__in'] = 'active,won';\n }\n\n $clOpportunities = $this->client->get('opportunity', $params);\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n foreach ($clOpportunities['data'] as $clOpportunity) {\n $stage = $this->config\n ->stages()\n ->where('crm_provider_id', $clOpportunity['status_id'])\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $clOpportunity['status_id']);\n }\n\n $record = [\n 'crmId' => $clOpportunity['id'],\n 'name' => $clOpportunity['note'],\n 'value' => currency_format($clOpportunity['value'], $clOpportunity['value_currency']),\n 'won' => $stage->probability === 100.00,\n 'closed' => $clOpportunity['status_type'] !== 'active',\n 'stage' => [\n 'id' => $stage->id_string,\n 'name' => $stage->name,\n ],\n 'recordType' => [],\n ];\n\n if ($ownerId && isset($clOpportunity['user_id']) && $clOpportunity['user_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n $crmId = null;\n\n if ($objectType === 'contact') {\n $contact = $this->syncContact($objectId);\n\n if ($contact && $contact->account_id) {\n $crmId = $contact->account->crm_provider_id;\n }\n } else {\n $crmId = $objectId;\n }\n\n if ($crmId) {\n $clTasks = $this->client->get('task', [\n 'lead_id' => $crmId,\n '_type' => 'lead',\n 'assigned_to' => $this->profile->crm_provider_id,\n 'is_complete' => 'false',\n '_order_by' => 'date',\n ]);\n\n foreach ($clTasks['data'] as $clTask) {\n $data[] = [\n 'crmId' => $clTask['id'],\n 'subject' => $clTask['text'],\n 'due' => $clTask['date'] ?? null,\n 'type' => null,\n ];\n }\n }\n\n return $data;\n }\n\n /**\n * Try to find email address in CRM service\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $clObject = $this->client->get('lead', [\n 'query' => 'sort:updated contact(email(email:\"' . $email . '\"))',\n '_limit' => 1,\n ]);\n\n if ($clObject['total_results'] > 0) {\n foreach ($clObject['data'][0]['contacts'] as $clContact) {\n foreach ($clContact['emails'] as $clEmail) {\n if ($email === $clEmail['email']) {\n $data = $this->convertCrmData($clContact['id'], $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n }\n }\n }\n\n return null;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n // Check if the user is internal.\n $teamMember = $this->team->users()->where('phone', $phone)->exists();\n\n // Skip the attendee if internal.\n if ($teamMember === false) {\n $clObject = $this->client->get('lead', [\n 'query' => 'sort:updated contact(' . $phone . ')',\n '_limit' => 1,\n ]);\n\n if ($clObject['total_results'] > 0) {\n foreach ($clObject['data'][0]['contacts'] as $clContact) {\n foreach ($clContact['phones'] as $clPhone) {\n if ($phone === $clPhone['phone']) {\n $data = $this->convertCrmData($clContact['id'], $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n }\n }\n }\n }\n\n return null;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $clObject = $this->client->get('lead', [\n 'query' => 'sort:updated contact(name:\"' . $name . '\")',\n '_limit' => 1,\n ]);\n\n if ($clObject['total_results'] > 0) {\n foreach ($clObject['data'][0]['contacts'] as $clContact) {\n if ($clContact['name'] === $name || $clContact['display_name'] === $name) {\n $data = $this->convertCrmData($clContact['id'], $userId);\n\n return ! empty(array_filter($data)) ? $data : false;\n }\n }\n }\n\n return false;\n });\n\n return is_array($result) ? $result : null;\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n private function convertCrmData(string $crmId, ?int $userId = null): array\n {\n $lead = null;\n $opportunity = null;\n $account = null;\n $stage = null;\n $countryCode = null;\n\n $contact = $this->syncContact($crmId);\n if ($contact) {\n $account = $contact->account;\n\n if ($contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account) {\n $countryCode = $account->country_code;\n }\n\n try {\n $cpOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId,\n );\n\n if (! empty($cpOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($cpOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception) {\n // Nothing to see here.\n }\n }\n\n return [\n $lead,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->type) {\n case Activity::TYPE_CONFERENCE:\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildCallPayload($activity);\n\n break;\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n private function mapStatus(string $status): string\n {\n switch ($status) {\n case Activity::STATUS_COMPLETED:\n case Activity::STATUS_IN_PROGRESS:\n case Activity::STATUS_FAILED:\n case Activity::STATUS_NO_ANSWER:\n case Activity::STATUS_BUSY:\n default:\n return $status;\n case Activity::STATUS_CANCELLED:\n return 'cancel';\n }\n }\n\n /**\n * @throws CrmException\n */\n private function buildCallPayload(Activity $activity): Activity\n {\n try {\n if ($activity->crm_provider_id) {\n // The activity should be logged under the existing Task (not Activity).\n $data = [\n '_type' => 'lead',\n 'lead_id' => $activity->account->crm_provider_id,\n 'assigned_to' => $this->profile->crm_provider_id,\n 'text' => $this->generateActivityDescription($activity),\n 'date' => $activity->getActualEndTime()->toDateString(),\n 'is_complete' => true,\n ];\n\n $this->logger->info('[Close CRM] Updating task', [\n 'activity' => $activity->id,\n 'crm_id' => $activity->crm_provider_id,\n 'data' => $data,\n ]);\n\n $this->client->put('task/' . $activity->crm_provider_id, $data);\n } else {\n // Just create an activity.\n $data = [\n 'lead_id' => $activity->account_id ? $activity->account->crm_provider_id : null,\n 'contact_id' => $activity->contact_id ? $activity->contact->crm_provider_id : null,\n 'created_by' => $this->profile->crm_provider_id,\n 'user_id' => $this->profile->crm_provider_id,\n 'direction' => $activity->getCrmType() === Activity::TYPE_SOFTPHONE ? 'outbound' : 'inbound',\n 'status' => $this->mapStatus($activity->getStatus()),\n 'note' => $this->generateActivityDescription($activity),\n 'duration' => $this->mapStatus($activity->getStatus()) !== 'cancel' ? $activity->duration : 0,\n 'phone' => $activity->to ? $activity->to->phone_number : null,\n ];\n\n $clActivity = $this->client->post('activity/call', $data);\n\n $this->logger->info('[Close CRM] Creating activity', [\n 'activity' => $activity->id,\n 'crm_id' => $clActivity['id'],\n 'data' => $data,\n 'response' => $clActivity,\n ]);\n\n $activity->crm_provider_id = $clActivity['id'];\n $activity->save();\n }\n } catch (ClientException $exception) {\n $response = $exception->getResponse();\n\n if ($response === null) {\n // Trying to debug weird cases where this is null.\n Sentry::captureException($exception);\n }\n\n $responseBody = $response->getBody();\n $message = $responseBody;\n $errorCode = $response->getStatusCode();\n\n $jsonResponse = json_decode($responseBody, true);\n if (isset($jsonResponse[0]['message'])) {\n $message = $jsonResponse[0]['message'];\n }\n\n throw new CrmException($message, $errorCode);\n }\n\n return $activity;\n }\n\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $clActivity = $this->client->post('activity/sms', [\n 'lead_id' => $activity->account_id ? $activity->account->crm_provider_id : null,\n 'contact_id' => $activity->contact_id ? $activity->contact->crm_provider_id : null,\n 'created_by' => $this->profile->crm_provider_id,\n 'user_id' => $this->profile->crm_provider_id,\n 'status' => $activity->getType() === Activity::TYPE_SMS_OUTBOUND ? 'sent' : 'inbox',\n 'text' => $this->generateActivityDescription($activity),\n 'remote_phone' => $activity->to ? $activity->to->phone_number : null,\n 'local_phone' => $activity->to ? $activity->to->phone_number : null,\n 'source' => 'Close.io',\n ]);\n\n $activity->crm_provider_id = $clActivity['id'];\n $activity->save();\n\n return $activity;\n }\n\n private function generateActivityDescription(Activity $activity): string\n {\n $description = '';\n\n switch ($activity->type) {\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n case Activity::TYPE_CONFERENCE:\n if ($activity->hasActivityType()) {\n $description = $activity->getActivityType()->getName() . PHP_EOL . PHP_EOL;\n }\n if ($activity->hasTitle()) {\n $description .= $activity->getTitle() . PHP_EOL;\n }\n\n if ($activity->hasReasonCodeBotKicked()) {\n $description .= 'Notetaker removed from this meeting' . PHP_EOL . PHP_EOL;\n // When we fix the state to be Activity::RECORDING_RECORDED as it should be this can change.\n } elseif ($activity->hasReasonCodeNotCompliant()) {\n $description .= 'Notetaker did not join due to recording consent not being provided by attendees' . PHP_EOL . PHP_EOL;\n } elseif ($activity->canReviewActivity()) {\n $playbackUrl = $activity->user->team->partner->getPlaybackUrl($activity);\n $description .= 'Review Activity:' . PHP_EOL . $playbackUrl . PHP_EOL . PHP_EOL;\n }\n\n if ($activity->type === Activity::TYPE_CONFERENCE) {\n $description .= 'Attendees:'\n . PHP_EOL\n . (new FilterJoinedParticipants())->toString($activity);\n }\n\n if (\\count($activity->notes) > 0) {\n $description .= PHP_EOL . PHP_EOL . 'Notes: ' . PHP_EOL;\n\n foreach ($activity->notes as $note) {\n $time = ($note->time > 3600) ? gmdate('H:i:s', $note->time) : gmdate('i:s', $note->time);\n $description .= $time . ' ' . $note->note . PHP_EOL;\n }\n }\n\n // Get all private messages.\n $messages = $activity->messages()\n ->where('is_private', 1)\n ->orderBy('created_at', 'asc');\n\n if ($messages->count() > 0) {\n $description .= PHP_EOL . PHP_EOL . 'Coaching Chat: ' . PHP_EOL;\n\n foreach ($messages->get() as $message) {\n $description .= $message->participant->name . ': ' . $message->message . PHP_EOL;\n }\n }\n\n // Get all public messages.\n $messages = $activity->messages()\n ->where('is_private', 0)\n ->orderBy('created_at', 'asc');\n\n if ($messages->count() > 0) {\n $description .= PHP_EOL . PHP_EOL . 'Customer Chat: ' . PHP_EOL;\n\n foreach ($messages->get() as $message) {\n $description .= $message->participant->name . ': ' . $message->message . PHP_EOL;\n }\n }\n\n if ($activity->summary) {\n $description .= PHP_EOL . PHP_EOL . 'Summary: ' . PHP_EOL . $activity->summary;\n }\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $description = $activity->description;\n\n break;\n }\n\n return $description;\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n // This is the user provided activity subject field.\n if (empty($fields['name'])) {\n return null;\n }\n\n $due = null;\n if (empty($fields['due_date']) === false) {\n $formatDue = Carbon::parse($fields['due_date']);\n $due = $formatDue->toDateTimeString();\n }\n\n $clTask = $this->client->post('task', [\n '_type' => 'lead',\n 'lead_id' => $activity->account->crm_provider_id,\n 'assigned_to' => $this->profile->crm_provider_id,\n 'text' => $fields['name'],\n 'date' => $due,\n 'is_complete' => false,\n ]);\n\n // We don't actually create a corresponding activity object on our side yet.\n return $clTask['id'];\n }\n\n /**\n * Store transcripts as note.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n if ($activity->account_id === null) {\n // We can only log to accounts (leads).\n return;\n }\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, self::NOTE_BODY_MAX_LENGTH);\n\n $clActivity = $this->client->post('activity/note', [\n 'lead_id' => $activity->account_id ? $activity->account->crm_provider_id : null,\n 'created_by' => $this->profile->crm_provider_id,\n 'user_id' => $this->profile->crm_provider_id,\n 'note' => $transcripts,\n ]);\n\n // Store CRM Activity ID in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $clActivity['id'];\n $transcription->save();\n }\n\n public function parseObjectType(string $objectId): string\n {\n if (Str::startsWith($objectId, 'lead')) {\n return 'account';\n }\n\n if (Str::startsWith($objectId, 'cont')) {\n return 'contact';\n }\n\n if (Str::startsWith($objectId, 'oppo')) {\n return 'opportunity';\n }\n\n throw new InvalidArgumentException('Unsupported Object Type');\n }\n\n /**\n * @inheritdoc\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n if ($crmObject instanceof Lead) {\n // This would never get invoked since we merge lead/accounts in Close.\n $this->client->put('lead/' . $crmObject->crm_provider_id, [\n 'status' => $stage->crm_provider_id,\n ]);\n } else {\n $this->client->put('opportunity/' . $crmObject->crm_provider_id, [\n 'status_id' => $stage->crm_provider_id,\n ]);\n }\n }\n\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $this->getClient()->updateObject($this->convertObjectTypeToResource($objectType), $objectId, $data);\n }\n\n public function prepareValueForUpdate(array $params): array\n {\n $convertedValue = $this->fieldValueConverter->convertToCrm(\n $this->config,\n $params['fieldName'],\n $params['fieldValue'],\n );\n\n if ($this->isCustomField($params['fieldName'])) {\n $params['fieldName'] = 'custom.' . $params['fieldName'];\n }\n\n $params['fieldValue'] = $convertedValue;\n\n return parent::prepareValueForUpdate($params);\n }\n\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n return $this->client->get($this->convertObjectTypeToResource($objectType) . '/' . $objectId);\n }\n\n /**\n *\n * @throws UnexpectedValueException\n */\n private function convertObjectTypeToResource(string $objectType): string\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return 'opportunity';\n\n case FieldData::OBJECT_CONTACT:\n return 'contact';\n\n case FieldData::OBJECT_ACCOUNT:\n return 'lead';\n\n case FieldData::OBJECT_TASK:\n return 'activity';\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $baseUrl = 'https://app.close.com/';\n $url = null;\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'lead/' . $providerId;\n\n break;\n\n case 'contact':\n $contact = $this->config->contacts()->where('crm_provider_id', $providerId)->first();\n if ($contact && $contact->account_id) {\n $url = $baseUrl . 'lead/' . $contact->account->crm_provider_id;\n }\n\n break;\n\n default:\n // Sadly we can't deeplink to anything else in Close UI.\n $url = null;\n }\n\n return $url;\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n return $this->transcriptionService\n ->findTranscriptionByActivity($activity)\n ->map(static function (array $transcriptionSegment): string {\n return $transcriptionSegment['formattedStartsAt'] . ' | ' . $transcriptionSegment['transcript'];\n })\n ->implode(PHP_EOL);\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $client = $this->getClient();\n $task = $client->get('task/' . $crmProviderId);\n\n return ! empty($task);\n } catch (HttpNotFoundException) {\n // Task not found in CRM - this is expected and permanent\n $this->logger->info('[Close] Task not found during verification', [\n 'task_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n } catch (CloseException $e) {\n // Handle 404 responses from Close API\n if ($e->getResponseStatusCode() === 404) {\n $this->logger->info('[Close] Task not found during verification (404)', [\n 'task_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n\n // Re-throw other Close exceptions for retry\n throw $e;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"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\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\ActivitySearch\\FilterDefinition;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinition;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQuery;\nuse Jiminny\\Component\\ActivitySearch\\FilterDefinitionQueryCollection;\nuse Jiminny\\Contracts\\ActivitySearch\\ValidatedFilterDefinitionInterface;\nuse Jiminny\\Contracts\\Repositories\\UserRepository;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Elastica\\Query;\n\nfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface\n{\n private const int NO_GROUP_ID = 999;\n private UserRepository $userRepository;\n\n public function __construct(UserRepository $userRepository)\n {\n $this->userRepository = $userRepository;\n }\n\n public function shouldApplyQueries(): bool\n {\n return count($this->getValue()) > 0;\n }\n\n public function getQueries(): FilterDefinitionQueryCollection\n {\n $userIds = $this->getValue();\n\n return FilterDefinitionQueryCollection::make([\n FilterDefinitionQuery::instance()\n ->setQuery(new Query\\Terms('coachingFeedbacks.coach.id_string', $userIds))\n ->setPath('coachingFeedbacks.coach', 'coachingFeedbacks'),\n ]);\n }\n\n public function toArray(): array\n {\n return [\n 'id' => 'coaching-feedback-user-filter',\n 'label' => 'Coach',\n 'helpText' => 'The person who completed the Coaching Framework',\n 'placeholder' => 'Search coaches',\n 'inputType' => InputTypeEnum::DROPDOWN_MULTIPLE,\n 'queryParam' => 'coaching_feedback_coach_id',\n 'options' => $this->getOptions(),\n 'groupLabelKey' => 'label',\n 'groupValuesKey' => 'users',\n 'optionLabelKey' => 'name',\n 'optionValueKey' => 'id',\n 'value' => $this->getValue(),\n ];\n }\n\n private function getOptions(): array\n {\n $user = $this->executedBy;\n $query = $user\n ->getTeam()\n ->users()\n ->whereNotIn(\n 'uuid',\n $this->userRepository->findInaccessibleDeactivatedUserUuids($user)\n ->map(static fn (string $uuid): string => RequiresUUID::toOptimized($uuid))\n ->all(),\n )\n ->orderBy('name');\n\n $users = $query\n ->with('group')\n ->get()\n ->reduce(\n static function (array $carry, Models\\User $user): array {\n $group = $user->getGroup();\n $groupId = $group !== null\n ? $group->getUuid()\n : self::NO_GROUP_ID;\n $groupName = $group !== null\n ? $group->getName()\n : '(NON-GROUPED)';\n\n if (! array_key_exists($groupId, $carry)) {\n $carry[$groupId] = [\n 'label' => $groupName,\n 'users' => [],\n ];\n }\n\n $carry[$groupId]['users'][] = [\n 'id' => $user->getUuid(),\n 'name' => sprintf(\n '%s%s',\n $user->getName(),\n ! $user->isStatusActive()\n ? ' (Inactive)'\n : ''\n ),\n ];\n\n return $carry;\n },\n []\n );\n\n return Collection::make($users)\n ->sortBy('label')\n ->values()\n ->toArray();\n }\n\n public function getValue(): array\n {\n if (! $this->criteria->hasCoachingFeedbackCoachUserIds()) {\n return $this->getDefaultValue();\n }\n\n return array_diff(\n $this->criteria->getCoachingFeedbackCoachUserId(),\n $this->userRepository->findInaccessibleDeactivatedUserUuids($this->executedBy)->all(),\n );\n }\n\n private function getDefaultValue(): array\n {\n return [];\n }\n\n public function getValidationRules(?string $prefix = null): array\n {\n $team = $this->executedBy->getTeam();\n\n return [\n $prefix . 'coaching_feedback_coach_id' => 'array',\n $prefix . 'coaching_feedback_coach_id.*' => 'uuid:users,team_id,' . $team->getId(),\n ];\n }\n\n public function getSortOrder(): int\n {\n return 86;\n }\n\n public function shouldBeIncluded(Team $team): bool\n {\n return $team->hasFeature(FeatureEnum::COACHING_FRAMEWORK);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"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}]...
|
-6754415607117048428
|
-9030663327281178587
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
39
5
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Close;
use Cache;
use Carbon\Carbon;
use Exception;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\CloseInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmMetadataInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\ServiceUnavailableException;
use Jiminny\Exceptions\UnexpectedCallException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Close\Processor\AccountProcessor;
use Jiminny\Services\Crm\Close\Processor\MetadataProcessor;
use Jiminny\Services\Crm\Close\Processor\OpportunityProcessor;
use Jiminny\Services\Crm\Close\Processor\StageProcessor;
use Jiminny\Services\Crm\Helpers\FilterJoinedParticipants;
use Jiminny\Services\Crm\Metadata\OpportunityMetadata;
use Jiminny\Services\Crm\Metadata\ProfileMetadata;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Sentry;
use UnexpectedValueException;
class Service extends BaseService implements
CloseInterface,
SyncCrmEntitiesInterface,
SyncCrmMetadataInterface,
RemoteEntityManipulationInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
SupportsObjectTypeParseInterface,
VerifyTaskExistsInterface
{
private const int NOTE_BODY_MAX_LENGTH = 3000000;
private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day
private StandardFieldMetadata $standardFieldMetadata;
private MetadataProcessor $metadataProcessor;
private FieldValueConverter $fieldValueConverter;
private StageProcessor $stageProcessor;
private OpportunityProcessor $opportunityProcessor;
private AccountProcessor $accountProcessor;
public function __construct(
Client $client,
StandardFieldMetadata $standardFieldMetadata,
MetadataProcessor $metadataProcessor,
FieldValueConverter $fieldValueConverter,
StageProcessor $stageResolver,
OpportunityProcessor $opportunityProcessor,
AccountProcessor $accountProcessor,
private readonly ProspectPhotoPathService $prospectPhotoPathService,
) {
parent::__construct();
$this->client = $client;
$this->standardFieldMetadata = $standardFieldMetadata;
$this->metadataProcessor = $metadataProcessor;
$this->fieldValueConverter = $fieldValueConverter;
$this->stageProcessor = $stageResolver;
$this->opportunityProcessor = $opportunityProcessor;
$this->accountProcessor = $accountProcessor;
}
public function getDisplayName(): string
{
return 'Close';
}
public function setConfiguration(Configuration $config): void
{
parent::setConfiguration($config);
$this->metadataProcessor->setConfiguration($config);
$this->stageProcessor->setConfiguration($config);
$this->opportunityProcessor->setConfiguration($config);
$this->accountProcessor->setConfiguration($config);
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
// In this case, the Account Owner is always the connection for any API operations.
return $user->getTeam()->getOwner()->getSocialAccount(SocialAccount::PROVIDER_CLOSE);
}
private function getClient(): Client
{
if (! $this->client instanceof Client) {
throw new UnexpectedCallException('Client not set');
}
return $this->client;
}
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return $this->fieldValueConverter->convertFromCrm($fieldType, $fieldValue);
}
protected function getFieldTypes(): array
{
return [
parent::OBJECT_OPPORTUNITY,
parent::OBJECT_CONTACT,
parent::OBJECT_ACCOUNT,
];
}
protected function getFields(string $crmObject): array
{
// not used
return [];
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {
$defaultFields = FieldDefinitions::defaultTaskFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
// Set up the activity field as the default Type.
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'type',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK];
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldData = FieldDefinitions::followupFieldsFilter();
foreach ($fieldData as $data) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
public function syncFields(): void
{
$this->syncStandardFields();
$this->syncCustomFields();
}
/**
* @important Works only for custom fields
*/
public function syncField(Field $field): void
{
$resource = $this->convertObjectTypeToResource($field->getObjectType());
// We can only sync custom fields in this CRM.
if ($this->isCustomField($field->getCrmProviderId()) === false) {
return;
}
$crmField = $this->getClient()->fetchCustomFieldDefinition($resource, $field->getCrmProviderId());
$this->metadataProcessor->syncField($crmField);
}
private function isCustomField(string $fieldId): bool
{
return strpos($fieldId, 'cf_') === 0;
}
/**
* @inheritdoc
*/
public function importPicklistValues(Field $field): array
{
// handled in syncFields()
return [];
}
/**
* @important We only support stages on the opportunity object
*
* @param string[]|null $types
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
if (! $missingStageName) {
// This is taken care of by syncOrganization()
return null;
}
$stage = $this->stageProcessor->resolveFromStageId($missingStageName);
if ($stage instanceof Stage) {
return $stage;
}
$stageMetadata = $this->getClient()->fetchStage($missingStageName);
if (! $stageMetadata) {
$this->logger->error('Stage does not exist', [
'stage' => $missingStageName,
]);
return null;
}
return $this->stageProcessor->importStage($stageMetadata);
}
/**
* @inheritdoc
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Even though Close.io has the concept of "leads", they fit more into our concept of accounts.
return 0;
}
/**
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Not a supported entity.
return null;
}
/**
* @throws Exception
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
foreach ($this->getClient()->listAccounts($since) as $clAccount) {
// Only sync if previously imported.
if ($this->hasAccount($clAccount->getId())) {
$this->importAccount($clAccount);
$syncCount++;
}
}
} catch (Exception $exception) {
$this->logger->error('Account sync failed', [
'error' => $exception->getMessage(),
]);
throw $exception;
}
return $syncCount;
}
public function syncAccount(string $crmId): ?Account
{
return $this->accountProcessor->syncAccount($crmId);
}
private function importAccount($crmData): Account
{
return $this->accountProcessor->importAccountMetadata($crmData);
}
/**
* @throws CloseException
*/
public function syncOpportunities(array $parameters, ?string $strategy = null): int
{
$strategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$strategies = $strategyResolver->getStrategies($this->config, $strategy);
$syncCount = 0;
try {
$opportunities = [];
foreach ($strategies as $syncStrategy) {
$opportunitiesData = $syncStrategy->fetchOpportunities($parameters);
$opportunities[] = $opportunitiesData['data'];
if ($opportunitiesData['has_more']) {
$this->logger->info('[Close] Sync Opportunities - count warning', [
'team_id' => $this->config->getTeam()->getId(),
'total' => $opportunitiesData['total'],
'count' => $opportunitiesData['count'],
'skip' => $opportunitiesData['skip'],
'strategies_count' => count($strategies),
]);
}
}
$opportunities = array_merge(...$opportunities);
} catch (CrmException $exception) {
$this->logger->error('Fetching opportunity data failed', [
'team' => $this->getTeam()->getSlug(),
'error' => $exception->getMessage(),
]);
return 0;
}
foreach ($opportunities as $opportunityMetadata) {
try {
$this->importOpportunity($opportunityMetadata);
$syncCount++;
} catch (Exception $exception) {
$this->logger->warning('Opportunity sync failed', [
'opportunity' => $opportunityMetadata->getId(),
'error' => $exception->getMessage(),
]);
}
}
return $syncCount;
}
public function syncOpportunity(string $crmId): ?Opportunity
{
$strategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
$strategy = $strategyResolver->resolve(
$this->config,
OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY,
);
$parameters = ['crm_id' => $crmId];
$opportunity = $strategy->fetchOpportunities($parameters);
if (empty($opportunity['data'])) {
return null;
}
return $this->importOpportunity($opportunity['data']);
}
private function importOpportunity(OpportunityMetadata $crmData): ?Opportunity
{
if (! $crmData->getLeadId()) {
$this->logger->warning('Opportunity does not have a lead ID', [
'opportunity' => $crmData->getId(),
]);
return null;
}
$account = $this->getConfiguration()
->accounts()
->where('crm_provider_id', $crmData->getLeadId())
->first();
if ($account === null) {
$account = $this->accountProcessor->syncAccount($crmData->getLeadId());
}
/** @var Profile $profile */
$profile = $this->getConfiguration()
->profiles()
->where('crm_provider_id', $crmData->getUserId())
->first();
$userId = $profile?->getUserId() ?? $account?->getUserId();
if ($userId === null) {
$this->logger->error('[Close] | Skip import, no user_id found', [
'id' => $crmData->getId(),
]);
return null;
}
$stage = $this->getConfiguration()
->stages()
->where('crm_provider_id', $crmData->getStageId())
->where('type', Stage::TYPE_OPPORTUNITY)
->first();
if ($stage === null) {
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $crmData->getStageId());
}
return $this->opportunityProcessor->import($crmData, $account, $stage, $profile);
}
/**
* @param array<string,string> $crmData
* @param string[] $crmFields
*/
public function importOpportunityCrmFieldData(array $crmData, array $crmFields, int $opportunityId): void
{
// handled in importOpportunity
}
/**
* @inheritdoc
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
/** No way to sync today.
$clContacts = $this->client->get('lead', [
'date_updated__gte' => $since->toDateString(),
'_order_by' => '-date_updated',
]);
foreach ($clContacts as $clContact) {
// Only sync if previously imported.
if ($this->hasContact($clContact['id'])) {
$this->importContact($clContact);
$syncCount++;
}
}
**/
} catch (Exception $exception) {
// Do nothing for now.
throw $exception;
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
$clContact = $this->client->get('contact/' . $crmId);
} catch (HttpNotFoundException $exception) {
return null;
}
return $this->importContact($clContact);
}
/**
* @inheritdoc
*/
private function importContact($crmData): Contact
{
$account = null;
if ($crmData['lead_id']) {
$account = $this->team
->accounts()
->where('crm_provider_id', $crmData['lead_id'])
->first();
if ($account === null) {
$account = $this->syncAccount($crmData['lead_id']);
}
}
$mobilePhone = $parsedNumber = null;
foreach ($crmData['phones'] as $phoneNumber) {
if ($phoneNumber['type'] === 'mobile') {
$mobilePhone = $this->buildContactMobilePhone(null, $phoneNumber['phone']);
} else {
$parsedNumber = $this->buildContactPhone(null, $phoneNumber['phone']);
}
}
$email = null;
if (empty($crmData['emails']) === false) {
$email = mb_strimwidth($crmData['emails'][0]['email'], 0, 80);
}
$profile = $this->config->profiles()->where('crm_provider_id', (string) $crmData['updated_by'])->first();
$data = [
'account_id' => $account->id ?? null,
'team_id' => $this->team->id,
'user_id' => $profile?->user_id,
'owner_id' => $crmData['updated_by'],
'name' => $crmData['name'] ?? 'Unknown',
'email' => $email,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobilePhone ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'title' => mb_strimwidth($crmData['title'] ?? '', 0, 128),
'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(
crmConfiguration: $this->config,
crmProviderId: $crmData['id'],
modelType: Contact::class,
fileName: $crmData['id'],
avatarText: $crmData['name'] ?? 'Unknown'
),
'remotely_created_at' => Carbon::parse($crmData['date_created']),
];
/** @var Contact */
return $this->config->contacts()->updateOrCreate(['crm_provider_id' => (string) $crmData['id']], $data);
}
private function buildContactPhone(?string $countryCode, ?string $number): ?array
{
if ($number) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($number, 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
return $parsedNumber;
}
private function buildContactMobilePhone(?string $countryCode, ?string $number): ?string
{
return $number ? mb_strimwidth(phone_e164($countryCode, $number), 0, 25) : null;
}
public function syncOrganization(): void
{
$organisation = $this->getClient()->fetchOrganisation();
$this->metadataProcessor->syncOrganisation($organisation);
foreach ($organisation->getPipelines() as $pipelineMetadata) {
$this->metadataProcessor->syncPipeline($pipelineMetadata);
}
}
private function syncStandardFields(): void
{
// Currently we sync only opportunity fields
$stages = $this->getClient()->listStages();
foreach ($this->standardFieldMetadata->opportunity($stages) as $fieldMetadata) {
$this->metadataProcessor->syncField($fieldMetadata);
}
$this->config->save();
}
private function syncCustomFields(): void
{
foreach ($this->getFieldTypes() as $fieldType) {
$objectType = $this->convertObjectTypeToResource($fieldType);
$currentFields = $this->getClient()->listCustomFieldDefinitions($objectType);
foreach ($currentFields as $fieldMetadata) {
$this->metadataProcessor->syncField($fieldMetadata);
}
}
$this->config->save();
}
public function syncProfiles(?User $userToSearch = null): ?Profile
{
/*
* Fetch the profile of the user from the database
* Then fetch the user metadata from Close and update it
* In case there's no profile for the user, proceed with syncing all users
*/
$foundUser = null;
if ($userToSearch) {
$profile = $userToSearch->getProfile();
if ($profile instanceof Profile) {
$crmProviderId = $profile->getCrmProviderId();
if ($crmProviderId) {
$profileMetadata = $this->getClient()->fetchUser($crmProviderId);
if (! $profileMetadata instanceof ProfileMetadata) {
return null;
}
return $this->metadataProcessor->syncProfile($profileMetadata);
}
}
}
foreach ($this->getClient()->listUsers() as $userMetadata) {
$userProfile = $this->metadataProcessor->syncProfile($userMetadata);
if (
$userToSearch instanceof User
&& $userProfile instanceof Profile
&& $userProfile->getUserId() === $userToSearch->getId()
) {
$foundUser = $userProfile;
}
}
return $foundUser;
}
public function syncProfileFields(): void
{
// Not used.
}
/**
* @inheritdoc
*/
public function find(string $name, array $scopes): array
{
$count = $this->limit ?? 20;
$offset = $this->offset ?? 0;
$data = Cache::remember($this->team->id . $name . $count . $offset, 300, function () use ($name, $count, $offset) {
$data = [];
try {
// If search phrase resembles phone number remove special symbols
if (preg_match('/^([0-9\s\-\+\(\)]*)$/', $name)) {
$name = '+' . preg_replace('/[\s\-\+\(\)]/', '', $name);
}
// Close do not provide a unified way to search, so we must hack our own.
$objects = $this->client->get('lead', [
'query' => 'sort:date_updated name:(' . $name . ') or email:(' . $name . ') or phone:(' . $name . ')',
'_limit' => $count, '_skip' => $offset,
]);
} catch (\GuzzleHttp\Exception\ServerException $exception) {
throw new ServiceUnavailableException($exception->getMessage());
}
foreach ($objects['data'] as $object) {
// We need a contact to dial it.
if (empty($object['contacts'])) {
continue;
}
foreach ($object['contacts'] as $contact) {
$record = [
'crmId' => $contact['id'],
'crmUrl' => $this->generateProviderUrl($object['id'], 'account'),
'name' => $contact['name'],
'industry' => null,
'title' => $contact['title'],
'organization' => $object['display_name'],
'prospectType' => 'contact',
'phoneNumbers' => [],
];
foreach ($contact['phones'] as $phone) {
if ($phone['type'] === 'mobile') {
$number = $this->buildContactMobilePhone(null, $phone['phone']);
$record['phoneNumbers'][] = [
'number' => $number,
'nationalFormat' => phone_national(null, $number),
'type' => 'mobile',
];
} else {
$parsedNumber = $this->buildContactPhone(null, $phone['phone']);
// Add phone number to record.
if (empty($parsedNumber['phone']) === false) {
$record['phoneNumbers'][] = [
'number' => $parsedNumber['phone'],
'nationalFormat' => phone_national(null, $parsedNumber['phone']),
'type' => 'phone',
];
}
}
}
$data[] = $record;
}
}
return $data;
});
return $data;
}
/**
* @inheritdoc
*/
public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array
{
$data = [];
$ownerData = [];
$ownerId = null;
$contact = null;
$account = null;
if ($crmAccountId) {
$account = $this->config->accounts()->where('crm_provider_id', $crmAccountId)->first();
if ($account === null) {
$account = $this->syncAccount($crmAccountId);
}
}
if ($crmContactId) {
$contact = $this->config->contacts()->where('crm_provider_id', $crmContactId)->first();
if ($contact === null) {
$contact = $this->syncContact($crmContactId);
}
}
if ($contact || $account) {
if ($contact && $account === null) {
$account = $contact->account;
}
if ($account === null) {
return [];
}
$params = [
'lead_id' => $account->crm_provider_id,
'_order_by' => '-date_updated',
];
$onlyOpen = true;
switch ($this->config->opportunity_assignment_rule) {
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:
$params['_order_by'] = '-date_updated';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:
$params['_order_by'] = '-date_created';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:
$params['_order_by'] = 'date_created';
break;
case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:
$params['_order_by'] = '-date_updated';
$onlyOpen = false;
}
if ($onlyOpen) {
$params['status_type__in'] = 'active,won';
}
$clOpportunities = $this->client->get('opportunity', $params);
if ($userId) {
$profileRepository = app(ProfileRepository::class);
$profile = $profileRepository->findProfileByUserId($this->config, $userId);
$ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;
}
foreach ($clOpportunities['data'] as $clOpportunity) {
$stage = $this->config
->stages()
->where('crm_provider_id', $clOpportunity['status_id'])
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $clOpportunity['status_id']);
}
$record = [
'crmId' => $clOpportunity['id'],
'name' => $clOpportunity['note'],
'value' => currency_format($clOpportunity['value'], $clOpportunity['value_currency']),
'won' => $stage->probability === 100.00,
'closed' => $clOpportunity['status_type'] !== 'active',
'stage' => [
'id' => $stage->id_string,
'name' => $stage->name,
],
'recordType' => [],
];
if ($ownerId && isset($clOpportunity['user_id']) && $clOpportunity['user_id'] === $ownerId) {
$ownerData[] = $record;
}
$data[] = $record;
}
if (! empty($ownerData)) {
return $ownerData;
}
}
return $data;
}
/**
* @inheritdoc
*/
public function getTasks(string $objectType, string $objectId, ?string $opportunityId): array
{
$data = [];
$crmId = null;
if ($objectType === 'contact') {
$contact = $this->syncContact($objectId);
if ($contact && $contact->account_id) {
$crmId = $contact->account->crm_provider_id;
}
} else {
$crmId = $objectId;
}
if ($crmId) {
$clTasks = $this->client->get('task', [
'lead_id' => $crmId,
'_type' => 'lead',
'assigned_to' => $this->profile->crm_provider_id,
'is_complete' => 'false',
'_order_by' => 'date',
]);
foreach ($clTasks['data'] as $clTask) {
$data[] = [
'crmId' => $clTask['id'],
'subject' => $clTask['text'],
'due' => $clTask['date'] ?? null,
'type' => null,
];
}
}
return $data;
}
/**
* Try to find email address in CRM service
*
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchExactlyByEmail(string $email, ?int $userId = null): ?array
{
$clObject = $this->client->get('lead', [
'query' => 'sort:updated contact(email(email:"' . $email . '"))',
'_limit' => 1,
]);
if ($clObject['total_results'] > 0) {
foreach ($clObject['data'][0]['contacts'] as $clContact) {
foreach ($clContact['emails'] as $clEmail) {
if ($email === $clEmail['email']) {
$data = $this->convertCrmData($clContact['id'], $userId);
return ! empty(array_filter($data)) ? $data : null;
}
}
}
}
return null;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}
*/
public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array
{
// Check if the user is internal.
$teamMember = $this->team->users()->where('phone', $phone)->exists();
// Skip the attendee if internal.
if ($teamMember === false) {
$clObject = $this->client->get('lead', [
'query' => 'sort:updated contact(' . $phone . ')',
'_limit' => 1,
]);
if ($clObject['total_results'] > 0) {
foreach ($clObject['data'][0]['contacts'] as $clContact) {
foreach ($clContact['phones'] as $clPhone) {
if ($phone === $clPhone['phone']) {
$data = $this->convertCrmData($clContact['id'], $userId);
return ! empty(array_filter($data)) ? $data : null;
}
}
}
}
}
return null;
}
/**
* @return null|array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
* }
*/
public function matchByName(string $name, ?int $userId = null): ?array
{
// Don't waste time searching for single character strings.
if (\strlen($name) <= 1) {
return null;
}
$cacheKey = $this->getCacheKey($name, $userId);
$result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {
$clObject = $this->client->get('lead', [
'query' => 'sort:updated contact(name:"' . $name . '")',
'_limit' => 1,
]);
if ($clObject['total_results'] > 0) {
foreach ($clObject['data'][0]['contacts'] as $clContact) {
if ($clContact['name'] === $name || $clContact['display_name'] === $name) {
$data = $this->convertCrmData($clContact['id'], $userId);
return ! empty(array_filter($data)) ? $data : false;
}
}
}
return false;
});
return is_array($result) ? $result : null;
}
protected function getCacheKey(string $object, ?int $userId = null): ?string
{
$key = $this->team->getId() . $object;
$keySuffix = $this->getOwnerKeySuffix($userId);
return $key . $keySuffix;
}
private function getOwnerKeySuffix(?int $userId = null): string
{
return $userId === null ? '' : (string) $userId;
}
private function convertCrmData(string $crmId, ?int $userId = null): array
{
$lead = null;
$opportunity = null;
$account = null;
$stage = null;
$countryCode = null;
$contact = $this->syncContact($crmId);
if ($contact) {
$account = $contact->account;
if ($contact->country_code) {
$countryCode = $contact->country_code;
} elseif ($account) {
$countryCode = $account->country_code;
}
try {
$cpOpportunities = $this->findOpportunities(
$account?->crm_provider_id,
$contact->crm_provider_id,
$userId,
);
if (! empty($cpOpportunities)) {
// Persist this remote object.
$opportunity = $this->syncOpportunity($cpOpportunities[0]['crmId']);
$stage = $opportunity?->getStage();
}
} catch (Exception) {
// Nothing to see here.
}
}
return [
$lead,
$account,
$opportunity,
$contact,
$stage,
$countryCode,
];
}
public function saveActivity(Activity $activity): Activity
{
switch ($activity->type) {
case Activity::TYPE_CONFERENCE:
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildCallPayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
private function mapStatus(string $status): string
{
switch ($status) {
case Activity::STATUS_COMPLETED:
case Activity::STATUS_IN_PROGRESS:
case Activity::STATUS_FAILED:
case Activity::STATUS_NO_ANSWER:
case Activity::STATUS_BUSY:
default:
return $status;
case Activity::STATUS_CANCELLED:
return 'cancel';
}
}
/**
* @throws CrmException
*/
private function buildCallPayload(Activity $activity): Activity
{
try {
if ($activity->crm_provider_id) {
// The activity should be logged under the existing Task (not Activity).
$data = [
'_type' => 'lead',
'lead_id' => $activity->account->crm_provider_id,
'assigned_to' => $this->profile->crm_provider_id,
'text' => $this->generateActivityDescription($activity),
'date' => $activity->getActualEndTime()->toDateString(),
'is_complete' => true,
];
$this->logger->info('[Close CRM] Updating task', [
'activity' => $activity->id,
'crm_id' => $activity->crm_provider_id,
'data' => $data,
]);
$this->client->put('task/' . $activity->crm_provider_id, $data);
} else {
// Just create an activity.
$data = [
'lead_id' => $activity->account_id ? $activity->account->crm_provider_id : null,
'contact_id' => $activity->contact_id ? $activity->contact->crm_provider_id : null,
'created_by' => $this->profile->crm_provider_id,
'user_id' => $this->profile->crm_provider_id,
'direction' => $activity->getCrmType() === Activity::TYPE_SOFTPHONE ? 'outbound' : 'inbound',
'status' => $this->mapStatus($activity->getStatus()),
'note' => $this->generateActivityDescription($activity),
'duration' => $this->mapStatus($activity->getStatus()) !== 'cancel' ? $activity->duration : 0,
'phone' => $activity->to ? $activity->to->phone_number : null,
];
$clActivity = $this->client->post('activity/call', $data);
$this->logger->info('[Close CRM] Creating activity', [
'activity' => $activity->id,
'crm_id' => $clActivity['id'],
'data' => $data,
'response' => $clActivity,
]);
$activity->crm_provider_id = $clActivity['id'];
$activity->save();
}
} catch (ClientException $exception) {
$response = $exception->getResponse();
if ($response === null) {
// Trying to debug weird cases where this is null.
Sentry::captureException($exception);
}
$responseBody = $response->getBody();
$message = $responseBody;
$errorCode = $response->getStatusCode();
$jsonResponse = json_decode($responseBody, true);
if (isset($jsonResponse[0]['message'])) {
$message = $jsonResponse[0]['message'];
}
throw new CrmException($message, $errorCode);
}
return $activity;
}
private function buildTextMessagePayload(Activity $activity): Activity
{
$clActivity = $this->client->post('activity/sms', [
'lead_id' => $activity->account_id ? $activity->account->crm_provider_id : null,
'contact_id' => $activity->contact_id ? $activity->contact->crm_provider_id : null,
'created_by' => $this->profile->crm_provider_id,
'user_id' => $this->profile->crm_provider_id,
'status' => $activity->getType() === Activity::TYPE_SMS_OUTBOUND ? 'sent' : 'inbox',
'text' => $this->generateActivityDescription($activity),
'remote_phone' => $activity->to ? $activity->to->phone_number : null,
'local_phone' => $activity->to ? $activity->to->phone_number : null,
'source' => 'Close.io',
]);
$activity->crm_provider_id = $clActivity['id'];
$activity->save();
return $activity;
}
private function generateActivityDescription(Activity $activity): string
{
$description = '';
switch ($activity->type) {
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
case Activity::TYPE_CONFERENCE:
if ($activity->hasActivityType()) {
$description = $activity->getActivityType()->getName() . PHP_EOL . PHP_EOL;
}
if ($activity->hasTitle()) {
$description .= $activity->getTitle() . PHP_EOL;
}
if ($activity->hasReasonCodeBotKicked()) {
$description .= 'Notetaker removed from this meeting' . PHP_EOL . PHP_EOL;
// When we fix the state to be Activity::RECORDING_RECORDED as it should be this can change.
} elseif ($activity->hasReasonCodeNotCompliant()) {
$description .= 'Notetaker did not join due to recording consent not being provided by attendees' . PHP_EOL . PHP_EOL;
} elseif ($activity->canReviewActivity()) {
$playbackUrl = $activity->user->team->partner->getPlaybackUrl($activity);
$description .= 'Review Activity:' . PHP_EOL . $playbackUrl . PHP_EOL . PHP_EOL;
}
if ($activity->type === Activity::TYPE_CONFERENCE) {
$description .= 'Attendees:'
. PHP_EOL
. (new FilterJoinedParticipants())->toString($activity);
}
if (\count($activity->notes) > 0) {
$description .= PHP_EOL . PHP_EOL . 'Notes: ' . PHP_EOL;
foreach ($activity->notes as $note) {
$time = ($note->time > 3600) ? gmdate('H:i:s', $note->time) : gmdate('i:s', $note->time);
$description .= $time . ' ' . $note->note . PHP_EOL;
}
}
// Get all private messages.
$messages = $activity->messages()
->where('is_private', 1)
->orderBy('created_at', 'asc');
if ($messages->count() > 0) {
$description .= PHP_EOL . PHP_EOL . 'Coaching Chat: ' . PHP_EOL;
foreach ($messages->get() as $message) {
$description .= $message->participant->name . ': ' . $message->message . PHP_EOL;
}
}
// Get all public messages.
$messages = $activity->messages()
->where('is_private', 0)
->orderBy('created_at', 'asc');
if ($messages->count() > 0) {
$description .= PHP_EOL . PHP_EOL . 'Customer Chat: ' . PHP_EOL;
foreach ($messages->get() as $message) {
$description .= $message->participant->name . ': ' . $message->message . PHP_EOL;
}
}
if ($activity->summary) {
$description .= PHP_EOL . PHP_EOL . 'Summary: ' . PHP_EOL . $activity->summary;
}
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$description = $activity->description;
break;
}
return $description;
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
// This is the user provided activity subject field.
if (empty($fields['name'])) {
return null;
}
$due = null;
if (empty($fields['due_date']) === false) {
$formatDue = Carbon::parse($fields['due_date']);
$due = $formatDue->toDateTimeString();
}
$clTask = $this->client->post('task', [
'_type' => 'lead',
'lead_id' => $activity->account->crm_provider_id,
'assigned_to' => $this->profile->crm_provider_id,
'text' => $fields['name'],
'date' => $due,
'is_complete' => false,
]);
// We don't actually create a corresponding activity object on our side yet.
return $clTask['id'];
}
/**
* Store transcripts as note.
*
* @throws Exception
*/
public function createTranscriptNotes(Activity $activity): void
{
if ($activity->account_id === null) {
// We can only log to accounts (leads).
return;
}
// Generate activity transcription.
$transcriptionData = $this->generateTranscription($activity);
// Truncate Notes with max notes length because transcription text could be very long.
$transcripts = mb_strimwidth($transcriptionData, 0, self::NOTE_BODY_MAX_LENGTH);
$clActivity = $this->client->post('activity/note', [
'lead_id' => $activity->account_id ? $activity->account->crm_provider_id : null,
'created_by' => $this->profile->crm_provider_id,
'user_id' => $this->profile->crm_provider_id,
'note' => $transcripts,
]);
// Store CRM Activity ID in transcription.
$transcription = $activity->getTranscription();
$transcription->crm_activity_id = $clActivity['id'];
$transcription->save();
}
public function parseObjectType(string $objectId): string
{
if (Str::startsWith($objectId, 'lead')) {
return 'account';
}
if (Str::startsWith($objectId, 'cont')) {
return 'contact';
}
if (Str::startsWith($objectId, 'oppo')) {
return 'opportunity';
}
throw new InvalidArgumentException('Unsupported Object Type');
}
/**
* @inheritdoc
*/
public function updateStage($crmObject, Stage $stage): void
{
if ($crmObject instanceof Lead) {
// This would never get invoked since we merge lead/accounts in Close.
$this->client->put('lead/' . $crmObject->crm_provider_id, [
'status' => $stage->crm_provider_id,
]);
} else {
$this->client->put('opportunity/' . $crmObject->crm_provider_id, [
'status_id' => $stage->crm_provider_id,
]);
}
}
public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void
{
$this->getClient()->updateObject($this->convertObjectTypeToResource($objectType), $objectId, $data);
}
public function prepareValueForUpdate(array $params): array
{
$convertedValue = $this->fieldValueConverter->convertToCrm(
$this->config,
$params['fieldName'],
$params['fieldValue'],
);
if ($this->isCustomField($params['fieldName'])) {
$params['fieldName'] = 'custom.' . $params['fieldName'];
}
$params['fieldValue'] = $convertedValue;
return parent::prepareValueForUpdate($params);
}
public function getRecord(string $objectType, string $objectId, array $fields = []): array
{
return $this->client->get($this->convertObjectTypeToResource($objectType) . '/' . $objectId);
}
/**
*
* @throws UnexpectedValueException
*/
private function convertObjectTypeToResource(string $objectType): string
{
switch ($objectType) {
case FieldData::OBJECT_OPPORTUNITY:
return 'opportunity';
case FieldData::OBJECT_CONTACT:
return 'contact';
case FieldData::OBJECT_ACCOUNT:
return 'lead';
case FieldData::OBJECT_TASK:
return 'activity';
default:
throw new UnexpectedValueException('Unsupported object type "' . $objectType . '"');
}
}
public function generateProviderUrl(string $providerId, string $objectType): ?string
{
$baseUrl = 'https://app.close.com/';
$url = null;
switch ($objectType) {
case 'account':
$url = $baseUrl . 'lead/' . $providerId;
break;
case 'contact':
$contact = $this->config->contacts()->where('crm_provider_id', $providerId)->first();
if ($contact && $contact->account_id) {
$url = $baseUrl . 'lead/' . $contact->account->crm_provider_id;
}
break;
default:
// Sadly we can't deeplink to anything else in Close UI.
$url = null;
}
return $url;
}
/**
* Generate transcription for the activity.
*/
private function generateTranscription(Activity $activity): string
{
if (! $this->config->store_transcript) {
// If sending transcription to activity toggle is disabled
return '';
}
return $this->transcriptionService
->findTranscriptionByActivity($activity)
->map(static function (array $transcriptionSegment): string {
return $transcriptionSegment['formattedStartsAt'] . ' | ' . $transcriptionSegment['transcript'];
})
->implode(PHP_EOL);
}
public function verifyTaskExists(Activity $activity): bool
{
$crmProviderId = $activity->getCrmProviderId();
$cacheKey = "crm_task_exists:{$this->config->getId()}:$crmProviderId";
return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {
try {
$client = $this->getClient();
$task = $client->get('task/' . $crmProviderId);
return ! empty($task);
} catch (HttpNotFoundException) {
// Task not found in CRM - this is expected and permanent
$this->logger->info('[Close] Task not found during verification', [
'task_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
...
|
54542
|
NULL
|
NULL
|
NULL
|
|
54545
|
1897
|
35
|
2026-05-18T13:14:56.789306+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110096789_m2.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIeWINavicarecodeLaravelFV faVsco.js°9 mas PhostormVIeWINavicarecodeLaravelFV faVsco.js°9 master kroledeym RinaCentralVideo• SalesforceIm Salesloft> D Talkdeskureams> D Telus>D Twilio_ IWillorlex_ I willorlexDireet• _ I willoVideo_Uploader› _ Vonagewxant1400mZoomBot> ZoomPhoneC) ActivitvcrmFieldsResolver.ohpC) ActivitvLoaService.oho© ActivitvProviderClient.ohn(C) ActivitvProviderRedistrv.oho© ActivityProviderService.php© CallDenormalizerRegistry.phpC) [EMAIL]© DatalmportHandlerInterface.php© MeetingBotService.php© ParticipantConsentService.php© ParticipantsService.phpT PecnonceValidation Trait nhnT SalesforceGetUserTrait.php- S/DenormaliserMainCrmDatalrait.onp@ TrackRecordinoFllesizeservice.one© TrackRecordingSizeEnforcer.phpT ValidateEmitProspectEventTrait.phpC AjReports0 AvatarMColondar0 Conference0 Crm>W Bullhornv closeOnoortunitvsvncstrateav• ProcessonProspectSearchStrateav• M TranslatorC) Client.ohr() CloseSxceotion.ohoC) FieldDefinitions.onvc) SieldValueConverter ohn•) Service nhnC) StandardFioldMetadata nhn> MConnen© SoftPhoneManager.php= custom.log= laravel.log« SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyC) CoreUserRequest.pnp© CoreUser.phpA console [STAGING]© CoachingFeedbackCoachUserln.php Xstohe©Crm/…../Service.php Xdeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*class Service extends BaseService 1mpLementsm A8 A39 M5 лV169 @184 0189 0 >194 0213222 ©242› use ...* ainherxcdocfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterfacepublic function getDefaultActivityField(string $activityType): Field(...}* oinnerztdoepublic function getsupportedPlavbooklvpeso: arnav-...public function getbealinsightsFields: arrayl...29 Ф >orotected function cetdefault-olLowuolavout-telds/strino sactivitvivoe: arravs...?34 0 >oublic function svncsieldso: void45 đ >Sthis->svncStandardFieldsoSthis->svncCustomFieldso* @important Works only for custom fieldspublic function syncField(Field $field): void$resource = $this->convert0bjectTypeToResource($field->get0bjectType):=230=131 ;-1 z0136 @t›I/ We can only sync custom fields in this CRM.if (Sthis->isCustomField(Sfield->getCrmProviderIdO) === false) {return:146 Gt >-150151 CSormField = Sthis-›getCLient()-›fetchCustomFieldDefinition(Sresource, field-ygetCrmProviderId 156sthas->metadata?rocessor->svnc-1eldscrmF1eLd)sorivate function iscustonsieldstrino Sfieldid: bool-...?* Oinheritdocpublic function importPicklistValues(Field $field): array{...}1usadeorivate const int No GROUP 10 = 9993 usagesorivate UserRenository SuserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection{...hpublic function toArray: array{...hprivate function getOptions(): array{...}public function getValue: array{...}private function getDefaultValue@: array{...}public function aetValidationRules(2strina Sorefix = null): arravs...?public function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}* [Platform] Refinemen... 46 m left100% 1• Mon 18 May 16:14:56U AskJiminnyReportActivityServiceTest vCascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSCIM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for methodw9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ATTR_ ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabled•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/Ueer/M PoleAttr.nhn +17-ites/User/ ẞ RoleAttrTest.nhn +224* Reiect alliiAccent alliAsk anvthina (&4-L)« Code SWF-1.6WN Windsurf Toams 222-50UTF.8io 4 spaces...
|
NULL
|
8201985752792080873
|
NULL
|
click
|
ocr
|
NULL
|
PhostormVIeWINavicarecodeLaravelFV faVsco.js°9 mas PhostormVIeWINavicarecodeLaravelFV faVsco.js°9 master kroledeym RinaCentralVideo• SalesforceIm Salesloft> D Talkdeskureams> D Telus>D Twilio_ IWillorlex_ I willorlexDireet• _ I willoVideo_Uploader› _ Vonagewxant1400mZoomBot> ZoomPhoneC) ActivitvcrmFieldsResolver.ohpC) ActivitvLoaService.oho© ActivitvProviderClient.ohn(C) ActivitvProviderRedistrv.oho© ActivityProviderService.php© CallDenormalizerRegistry.phpC) [EMAIL]© DatalmportHandlerInterface.php© MeetingBotService.php© ParticipantConsentService.php© ParticipantsService.phpT PecnonceValidation Trait nhnT SalesforceGetUserTrait.php- S/DenormaliserMainCrmDatalrait.onp@ TrackRecordinoFllesizeservice.one© TrackRecordingSizeEnforcer.phpT ValidateEmitProspectEventTrait.phpC AjReports0 AvatarMColondar0 Conference0 Crm>W Bullhornv closeOnoortunitvsvncstrateav• ProcessonProspectSearchStrateav• M TranslatorC) Client.ohr() CloseSxceotion.ohoC) FieldDefinitions.onvc) SieldValueConverter ohn•) Service nhnC) StandardFioldMetadata nhn> MConnen© SoftPhoneManager.php= custom.log= laravel.log« SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyC) CoreUserRequest.pnp© CoreUser.phpA console [STAGING]© CoachingFeedbackCoachUserln.php Xstohe©Crm/…../Service.php Xdeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*class Service extends BaseService 1mpLementsm A8 A39 M5 лV169 @184 0189 0 >194 0213222 ©242› use ...* ainherxcdocfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterfacepublic function getDefaultActivityField(string $activityType): Field(...}* oinnerztdoepublic function getsupportedPlavbooklvpeso: arnav-...public function getbealinsightsFields: arrayl...29 Ф >orotected function cetdefault-olLowuolavout-telds/strino sactivitvivoe: arravs...?34 0 >oublic function svncsieldso: void45 đ >Sthis->svncStandardFieldsoSthis->svncCustomFieldso* @important Works only for custom fieldspublic function syncField(Field $field): void$resource = $this->convert0bjectTypeToResource($field->get0bjectType):=230=131 ;-1 z0136 @t›I/ We can only sync custom fields in this CRM.if (Sthis->isCustomField(Sfield->getCrmProviderIdO) === false) {return:146 Gt >-150151 CSormField = Sthis-›getCLient()-›fetchCustomFieldDefinition(Sresource, field-ygetCrmProviderId 156sthas->metadata?rocessor->svnc-1eldscrmF1eLd)sorivate function iscustonsieldstrino Sfieldid: bool-...?* Oinheritdocpublic function importPicklistValues(Field $field): array{...}1usadeorivate const int No GROUP 10 = 9993 usagesorivate UserRenository SuserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection{...hpublic function toArray: array{...hprivate function getOptions(): array{...}public function getValue: array{...}private function getDefaultValue@: array{...}public function aetValidationRules(2strina Sorefix = null): arravs...?public function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}* [Platform] Refinemen... 46 m left100% 1• Mon 18 May 16:14:56U AskJiminnyReportActivityServiceTest vCascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSCIM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for methodw9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ATTR_ ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabled•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/Ueer/M PoleAttr.nhn +17-ites/User/ ẞ RoleAttrTest.nhn +224* Reiect alliiAccent alliAsk anvthina (&4-L)« Code SWF-1.6WN Windsurf Toams 222-50UTF.8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
54544
|
1896
|
65
|
2026-05-18T13:14:56.789905+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110096789_m1.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp(Platform) Refinemen…46 m leftmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.comNikolay Yankov (Presenting)8• Mon 18 May 16:14:566Mon 18 May 16:14$**(18® For you© Recent|# Starred8? Apps07 Spacesacarco59 Service-DeskJiminny (Oid)Jiminny (New)I CD Ptatform TeamCID Capture TeamIID Enterprise Stabllty L(ID Processing TeamIID SE Kanban|= More spaces= FiitersCB Dashboards® OperationsE3 Confluence38 Team.% Customise sidebarC Ate© Ours* CiauCoss aopuAlowPpe0m 0 sm2 tY-2 1Y-https://fiminny.atlassian.net/browse/JY-20879- JchyC Projects|23 SSH• Home | Salestorce08DD0©M Ce сrско!Sentry*xIQ Search |Spaces Jminny (New)Add parent4JY-20679Enable users to use their new activity typesDescriptionCurrently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomacons that we have - this means they need to copy au of their prompes agan, wrich takes a nuge amourk or üreFor SF, Hubspot, Copper, Bullhorn, Close and Pipedrive (anything apart from Zoho)|• when a new activity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to all the playbooks where activity type field isCheteu• if a customer manuaty creates an activity type in Jiminny - show them a warning message on the top saying "If you want this activity type to syng to your CRM oneach call - make sure to have i& in your CRM with Ene exact same name*" - use this .https://www.figma.com/design/XcUety9m5Fiz8KosLAUn/Project-Phoenix2node-id=7483-2600188t-uJOZVbpzYIpCSPAy-1 [Connect your Fioma account• the message should stay onty while they are adding the activity type - I they leave or refresh the page it should disappear. Use the info message that we haveSubtasksAdd SudCasKLinked work itemsAdd linked work item~ ActivityWork logSuggest a reply….Who is working on this...?Status update.Pro tip: press (M to comment7 EY-* Prol© smnsigits & Coachn.C DevC UXAsk RovoBackiogy# Improve Story~ DetailsAssignee8 UnassignedAssign to meKUpOrtE& Galya Dimitrova• Quick start developmentLink this work item to your code byincluding keys when creatingabranch, commit, or pull requestDismissycomenQ Open with VS CodeCreate branch*Create commit)ComponentsPlatformNoneStory point estimatoNikolay YankovNikolay IvanovAneliya Angelova•Galya DimitrovaLukas Kovalik1:01:234:14 PM | [Platform] Refinement ®Lộ3...
|
NULL
|
-55588608043366058
|
NULL
|
click
|
ocr
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp(Platform) Refinemen…46 m leftmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.comNikolay Yankov (Presenting)8• Mon 18 May 16:14:566Mon 18 May 16:14$**(18® For you© Recent|# Starred8? Apps07 Spacesacarco59 Service-DeskJiminny (Oid)Jiminny (New)I CD Ptatform TeamCID Capture TeamIID Enterprise Stabllty L(ID Processing TeamIID SE Kanban|= More spaces= FiitersCB Dashboards® OperationsE3 Confluence38 Team.% Customise sidebarC Ate© Ours* CiauCoss aopuAlowPpe0m 0 sm2 tY-2 1Y-https://fiminny.atlassian.net/browse/JY-20879- JchyC Projects|23 SSH• Home | Salestorce08DD0©M Ce сrско!Sentry*xIQ Search |Spaces Jminny (New)Add parent4JY-20679Enable users to use their new activity typesDescriptionCurrently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomacons that we have - this means they need to copy au of their prompes agan, wrich takes a nuge amourk or üreFor SF, Hubspot, Copper, Bullhorn, Close and Pipedrive (anything apart from Zoho)|• when a new activity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to all the playbooks where activity type field isCheteu• if a customer manuaty creates an activity type in Jiminny - show them a warning message on the top saying "If you want this activity type to syng to your CRM oneach call - make sure to have i& in your CRM with Ene exact same name*" - use this .https://www.figma.com/design/XcUety9m5Fiz8KosLAUn/Project-Phoenix2node-id=7483-2600188t-uJOZVbpzYIpCSPAy-1 [Connect your Fioma account• the message should stay onty while they are adding the activity type - I they leave or refresh the page it should disappear. Use the info message that we haveSubtasksAdd SudCasKLinked work itemsAdd linked work item~ ActivityWork logSuggest a reply….Who is working on this...?Status update.Pro tip: press (M to comment7 EY-* Prol© smnsigits & Coachn.C DevC UXAsk RovoBackiogy# Improve Story~ DetailsAssignee8 UnassignedAssign to meKUpOrtE& Galya Dimitrova• Quick start developmentLink this work item to your code byincluding keys when creatingabranch, commit, or pull requestDismissycomenQ Open with VS CodeCreate branch*Create commit)ComponentsPlatformNoneStory point estimatoNikolay YankovNikolay IvanovAneliya Angelova•Galya DimitrovaLukas Kovalik1:01:234:14 PM | [Platform] Refinement ®Lộ3...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
54546
|
1896
|
66
|
2026-05-18T13:14:58.428583+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110098428_m1.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - [Platform] Refinement 🔍","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.8411111,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.0,"top":0.8794444,"width":0.033680554,"height":0.03888889},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.0,"top":0.91833335,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.95666665,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Nikolay Yankov (Presenting)","depth":12,"bounds":{"left":0.07534722,"top":0.101111114,"width":0.124305554,"height":0.022222223},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Yankov (Presenting)","depth":13,"bounds":{"left":0.07534722,"top":0.10222222,"width":0.124305554,"height":0.020555556},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.88680553,"top":0.08944444,"width":0.04097222,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3657525577202427294
|
3314779464157905542
|
visual_change
|
hybrid
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp> 0.§ [Platform] Refinemen... 46 m left100% C8• Mon 18 May 16:14:58meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.comNikolay Yankov (Presenting)=+Mon 18 May 16:14$(18® For you© Recent# Starred8? Apps0 Spacessaire(9 Service-DeskJiminny (Oid)Jiminny (New)1 CID Platform TeamCID Capture TeamIID Enterprise Stabllity L.(ID Processing TeamIID SE Kanban= More spaces= FltersCB Dashboards® Oporations83 Confluence3: Team.2: Customise sidebanC ADe© CursM Cтau• Com 0 300- C Alor3 Ppe0 3mon JitiC Projects|E3 SSH• Home | SalesforceE DatadogClaudelSentrySpaces Jminny (New)Add parem4JY-20579)Enable users to use their new activity types• DescriptionCurrently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomatons that we have - tis means they need to copy an of their prompts agan. Wrich takes a nuge amourk or tabeFor SF, Hubspot, Copper, Buflhorn, Close and Pipedrive (anything apart from Zoho)• when a new activity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to alf the playbooks where activity type field ischener• it a customer manualy creates an activity type in Jiminny - show them a warning message on the top saying "II you want this activity type to sync to your CRM oneach call - make sure to have it in your CRM with Ine exact same name* - use this f http://www.figma.com/design/XcUety9mx5Fiz8KosLAUn/Project-Phoenixinode-id=7483-26001881=UOZVbpzYIpC5PAy-1 [Connect your Fioma account)• the message should stay only while they are adding the activity type - if they leave or refresh the page it should disappear. Use the info message that we haveSubtasksAdd SudraskLinked work itemsAdd linked work femActivityHistoryWork logoeAconentneSuggest a reply…Who is working on this…..?Status updatePro tip: press M to commerZ LY-P ProlL Insights & Coachin..Ask RovoBackiogyXI Improve Story|• DetailsAssignee8 UnassignedAssign to meKUрOrtE& Galya DimitrovaQuick start developmentLink this work item to your code byincluding keys when creatingabranch, commit, or pull requestDismissOycocnie@ Open with VS CodeCreate branch+ Create commitComponentsAole000wotkStory point estimateNikolay YankovNikolay IvanovAneliya AngelovaGalya Dimitrova4:14 PM | [Platform] Refinement®Lukas Kovalik1:01:25Lộ3...
|
54544
|
NULL
|
NULL
|
NULL
|
|
54547
|
1896
|
67
|
2026-05-18T13:15:14.162792+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110114162_m1.jpg...
|
PhpStorm
|
faVsco.js – Crm/…/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp> 0.§ [Platform] Refinemen... 45 m left100% <78• Mon 18 May 16:15:14meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com=Nikolay Yankov (Presenting)+Mon 18 May 16:16$*(18® For you© Recent|# Starred8? Apps0 Spacesaurco(9 Service-DeskJiminny (Oid)Jiminny (New)1 CID Platform TeamCID Capture Team|IID Enterprise Stabllity L.(ID Processing TeamIID SE Kanban= More spaces=FiltersCB Dashboards® OperationsE3 Confluence3: Team-% Customise sidebarC Atte© Ours* CіauCos:© a0pC AlOIPipe0 miZ UY-C Projects|E3 SSH• Home | Salestorce00 8.Datadog*xInsignts & Coachn.C DevSearch+ CreateAsk RovoSpacesJminny (New)Add paremEnable users to use their new activity typesBackiogyIX Improve Story|Details• DescriptionCurrently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomacons that we have - this means they need to copy an of their prompes egan. wnich takes à nuge amourk or üreFor SF, Hubspot, Copper, Bullhorn, Close and Pipedrive (anything apart from Zoho)• when a new activity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to all the playbooks where activity type field isChenesu• if a customer manualy creates an activity type in Jiminny - show them a warning message on the top saying "If you want this activity type to sync to your CRM onesch call - make sure to have it in your CRM with dhe exact same name" - use this F https://www.figma.com/design/XcUe1y9mxSFiz8KosLAUn/Project-PhoenbCnode-id=7483-2600188t-uJOZVbpzYIpC5PAy-1 [Connect your Fioma ascount• the message should stay only while they are adding the activity type - il they leave or refresh the page it should disappear. Use the info message that we haveAssignee& UnassignedAssign to meKUpоrtE& Galya DimitrovaQuick start developmentLink this work item to your code byincluding keys when creating abranch, commit, or pull requestSubtasksDismissycoemenAdd sudtasxQ Open with VS CodeLinked work itemsCreate branchAdd linked work item* Create commitl~ ActivityComponentsPlatformWork logSuggest a reply.….Who is working on this...?Status update.Pro tip: press (M to commentNoneStory point estimatoNikolay YankovNikolay IvanovAneliya Angelova•Galya Dimitrova4:15 PM | [Platform] Refinement ®Lukas Kovalik1:01:41...
|
NULL
|
-8860704730790595634
|
NULL
|
click
|
ocr
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp> 0.§ [Platform] Refinemen... 45 m left100% <78• Mon 18 May 16:15:14meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com=Nikolay Yankov (Presenting)+Mon 18 May 16:16$*(18® For you© Recent|# Starred8? Apps0 Spacesaurco(9 Service-DeskJiminny (Oid)Jiminny (New)1 CID Platform TeamCID Capture Team|IID Enterprise Stabllity L.(ID Processing TeamIID SE Kanban= More spaces=FiltersCB Dashboards® OperationsE3 Confluence3: Team-% Customise sidebarC Atte© Ours* CіauCos:© a0pC AlOIPipe0 miZ UY-C Projects|E3 SSH• Home | Salestorce00 8.Datadog*xInsignts & Coachn.C DevSearch+ CreateAsk RovoSpacesJminny (New)Add paremEnable users to use their new activity typesBackiogyIX Improve Story|Details• DescriptionCurrently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomacons that we have - this means they need to copy an of their prompes egan. wnich takes à nuge amourk or üreFor SF, Hubspot, Copper, Bullhorn, Close and Pipedrive (anything apart from Zoho)• when a new activity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to all the playbooks where activity type field isChenesu• if a customer manualy creates an activity type in Jiminny - show them a warning message on the top saying "If you want this activity type to sync to your CRM onesch call - make sure to have it in your CRM with dhe exact same name" - use this F https://www.figma.com/design/XcUe1y9mxSFiz8KosLAUn/Project-PhoenbCnode-id=7483-2600188t-uJOZVbpzYIpC5PAy-1 [Connect your Fioma ascount• the message should stay only while they are adding the activity type - il they leave or refresh the page it should disappear. Use the info message that we haveAssignee& UnassignedAssign to meKUpоrtE& Galya DimitrovaQuick start developmentLink this work item to your code byincluding keys when creating abranch, commit, or pull requestSubtasksDismissycoemenAdd sudtasxQ Open with VS CodeLinked work itemsCreate branchAdd linked work item* Create commitl~ ActivityComponentsPlatformWork logSuggest a reply.….Who is working on this...?Status update.Pro tip: press (M to commentNoneStory point estimatoNikolay YankovNikolay IvanovAneliya Angelova•Galya Dimitrova4:15 PM | [Platform] Refinement ®Lukas Kovalik1:01:41...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
54548
|
1897
|
36
|
2026-05-18T13:15:14.163149+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110114163_m2.jpg...
|
PhpStorm
|
faVsco.js – Crm/…/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PnostormVIewINavicatecodeFV faVsco.js°9 master kPr PnostormVIewINavicatecodeFV faVsco.js°9 master kProiectRinaCentralVideoSalesforceIm Salesloft> D TalkdeskDTeams> D Telus>D Twilio>@ TwilioFlex_ I willorlexDireet• _ willoVidec_Uploader› _ Vonagewxant• 400mZoomBot> ZoomPhoneC) ActivitvcrmFieldsResolver.ohpC) ActivitvLoaService.oho© ActivitvProviderClient.ohp(C) ActivitvProviderRedistrv.oho© ActivityProviderService.php© CallDenormalizerRegistry.phpC) CrmOwnerResolver.oho© DatalmportHandlerInterface.php© MeetingBotService.php© ParticipantConsentService.php© ParticipantsService.phpT PecnonceValidation Trait nhnT SalesforceGetUserTrait.php- S/DenormaliserMainCrmDatalrait.onp@ TrackRecordinoFllesizeservice.one© TrackRecordingSizeEnforcer.phpT ValidateEmitProspectEventTrait.phpC AjReports0 AvatarMcolondar0 Conference0 Crm> C Bullhornv closeOooortunitvsvncStrateav• ProcessonProspectSearchStrateav• M TranslatorC) Client.oho() CloseSxceotion.ohoC) FieldDefinitions.onvc) SieldValueConverter ohn•) Service nhnC) StandardFioldMetadata nhn> MConnen© SoftPhoneManager.php= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]& console [EU]C) CoreUserRequest.pnp© CoreUser.phpA console [STAGING]© CoachingFeedbackCoachUserln.php Xstohe© Crm/../Service.php xdeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*class Service extends BaseService 1mpLementsm A8 A39 M5 лV169 @184 (0'189 0 >194 0213222 ©› use ...* ainherxcdocfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterfacepublic function getDefaultActivityField(string $activityType): Field(...}* oinnerztdocpublic function getsupportedPlavbooklvpeso: arnav-...public function getbealinsightsFields: arrayl...29 Ф >orotected function cetdefault-olLowuolavout-telds/strino sactivitvivoe: arravs...?34 0 >oublic function svncsieldso: void45 đ >Sthis->svncStandardFieldsoSthis->svncCustomFieldso* @important Works only for custom fieldspublic function syncField(Field $field): void=230=131 ;-1 z0$resource = $this->convert0bjectTypeToResource($field->get0bjectType):136 @t›I/ We can only sync custom fields in this CRM.if (Sthis->isCustomField(Sfield->getCrmProviderIdO) === false) {return:146 Gt >-150151 CSormField = Sthis-›getCLient()-›fetchCustomFieldDefinition(Sresource, field-ygetCrmProviderId 156sthas->metadata?rocessor->svnc-1eldscrmF1eLd)sorivate function iscustonsieldstrino Sfieldid: bool-...?* Oinheritdocpublic function importPicklistValues(Field $field): array{...}usadeorivate const int No GROUP 10 = 9993 usagesorivate UserRenository SuserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection{...hpublic function toArray: array{...hprivate function getOptions(): array{...}public function getValue: array{...}lprivate function getDefaultValue@: array{...}public function aetValidationRules(2strina Sorefix = null): arravs...?public function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}40m let• мon 10 May 10.10.14cascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSClM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for method9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ATTR_ ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabled•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/Ueer/M PoleAttr.nhn +17-ites/User/ ẞ RoleAttrTest.nhn +224* Reiect allAccent alliAsk anvthina (&4-L)« Code SWF-1.6222-50UTE..fo 4 spaces...
|
NULL
|
5254158447170114025
|
NULL
|
click
|
ocr
|
NULL
|
PnostormVIewINavicatecodeFV faVsco.js°9 master kPr PnostormVIewINavicatecodeFV faVsco.js°9 master kProiectRinaCentralVideoSalesforceIm Salesloft> D TalkdeskDTeams> D Telus>D Twilio>@ TwilioFlex_ I willorlexDireet• _ willoVidec_Uploader› _ Vonagewxant• 400mZoomBot> ZoomPhoneC) ActivitvcrmFieldsResolver.ohpC) ActivitvLoaService.oho© ActivitvProviderClient.ohp(C) ActivitvProviderRedistrv.oho© ActivityProviderService.php© CallDenormalizerRegistry.phpC) CrmOwnerResolver.oho© DatalmportHandlerInterface.php© MeetingBotService.php© ParticipantConsentService.php© ParticipantsService.phpT PecnonceValidation Trait nhnT SalesforceGetUserTrait.php- S/DenormaliserMainCrmDatalrait.onp@ TrackRecordinoFllesizeservice.one© TrackRecordingSizeEnforcer.phpT ValidateEmitProspectEventTrait.phpC AjReports0 AvatarMcolondar0 Conference0 Crm> C Bullhornv closeOooortunitvsvncStrateav• ProcessonProspectSearchStrateav• M TranslatorC) Client.oho() CloseSxceotion.ohoC) FieldDefinitions.onvc) SieldValueConverter ohn•) Service nhnC) StandardFioldMetadata nhn> MConnen© SoftPhoneManager.php= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]& console [EU]C) CoreUserRequest.pnp© CoreUser.phpA console [STAGING]© CoachingFeedbackCoachUserln.php Xstohe© Crm/../Service.php xdeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*class Service extends BaseService 1mpLementsm A8 A39 M5 лV169 @184 (0'189 0 >194 0213222 ©› use ...* ainherxcdocfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterfacepublic function getDefaultActivityField(string $activityType): Field(...}* oinnerztdocpublic function getsupportedPlavbooklvpeso: arnav-...public function getbealinsightsFields: arrayl...29 Ф >orotected function cetdefault-olLowuolavout-telds/strino sactivitvivoe: arravs...?34 0 >oublic function svncsieldso: void45 đ >Sthis->svncStandardFieldsoSthis->svncCustomFieldso* @important Works only for custom fieldspublic function syncField(Field $field): void=230=131 ;-1 z0$resource = $this->convert0bjectTypeToResource($field->get0bjectType):136 @t›I/ We can only sync custom fields in this CRM.if (Sthis->isCustomField(Sfield->getCrmProviderIdO) === false) {return:146 Gt >-150151 CSormField = Sthis-›getCLient()-›fetchCustomFieldDefinition(Sresource, field-ygetCrmProviderId 156sthas->metadata?rocessor->svnc-1eldscrmF1eLd)sorivate function iscustonsieldstrino Sfieldid: bool-...?* Oinheritdocpublic function importPicklistValues(Field $field): array{...}usadeorivate const int No GROUP 10 = 9993 usagesorivate UserRenository SuserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection{...hpublic function toArray: array{...hprivate function getOptions(): array{...}public function getValue: array{...}lprivate function getDefaultValue@: array{...}public function aetValidationRules(2strina Sorefix = null): arravs...?public function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}40m let• мon 10 May 10.10.14cascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSClM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for method9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ATTR_ ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabled•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/Ueer/M PoleAttr.nhn +17-ites/User/ ẞ RoleAttrTest.nhn +224* Reiect allAccent alliAsk anvthina (&4-L)« Code SWF-1.6222-50UTE..fo 4 spaces...
|
54545
|
NULL
|
NULL
|
NULL
|
|
54549
|
1896
|
68
|
2026-05-18T13:15:16.790621+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110116790_m1.jpg...
|
PhpStorm
|
faVsco.js – Crm/…/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-943456739835505562
|
-8636494053633454656
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp:[Platform) Refinemen….45 m leftmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com8 • Mon 18 May 16:15:16Nikolay Yankov (Presenting)Mon 18 May 16:16$(18® For you© Recent|# Starred8? Apps0 Spacesaurco(9 Service-DeskJiminny (Oid)Jiminny (New)1 CID Platform TeamCID Capture Team |IID Enterprise Stabllity L.CID Processing TeamIID SE Kanban= More spaces= FitersB Dashboards€ OperationsE3 Confluence2: Teamt% Customise sidebarC Ate© Ours* СаCost© a0pC AlOIPpe0 3m0 smioJimiC Projects|E SSH• Home | SalesforceE Datadog* Claudel*xISearchSpacesJminny (New)Add parentEnable users to use their new activity typesDescriptionCurrently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomatons that we have - this means they need to copy an of their prompes egan. wrich takes à nuge amourk or treFor SF, Hubspot, Copper, Bullhorn, Close and Pipedrive (anything apart from Zoho)• when a new activity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to all the playbooks where activity type field isCheneru• if a customer manualy creates an activity type in Jiminny - show them a warning message on the top saying "If you want this activity type to sync to your CRM oneach call - make sure to have ik in your CRM with lne exact same name*" - use this https://www.figma.com/design/XcUety9mx5Fiz8KosLAUn/Project-Phoenix2node-id=7483-2600188t-uOZVbpzYIpC5PAy-1 Connect your Fioma account• the message should stay only while they are adding the activity type - il they leave or refresh the page it should disappear. Use the info message that we haveSubtasksAdd subtaskLinked work itemsAdd linked work item~ ActivityWork logSuggest a reply.…..Who is working on this…..?Status update.Pro tip: press (M to commentZ tY-insights & CoachinC DevAsk RovoBackiogyIX Improve Story|DetailsAssignee& UnassignedAssign to meKoportet& Galya DimitrovaQuick start developmentLink this work item to your code byincluding keys when creatingabranch, commit, or pull requestDismissycoemet@ Open with VS CodeCreate branch* Create commitlComponentsPlatformSo-rtoNoneStory point estimatoNikolay YankovNikolay IvanovAneliya AngelovaGalya Dimitrova4:15 PM | [Platform] Refinement ®Lukas Kovalik1:01:44...
|
54547
|
NULL
|
NULL
|
NULL
|
|
54551
|
1897
|
37
|
2026-05-18T13:15:20.975662+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110120975_m2.jpg...
|
PhpStorm
|
faVsco.js – Crm/…/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormFV faVsco.js vProjectVIeWg9 master kNaviga PhostormFV faVsco.js vProjectVIeWg9 master kNavigateCodeLaravelKeractorRinaCentralVideoO SalesforceIm Salesloft> D Talkdeskureams> D Telus> D Twilio_ IWillorlex_ I willorlexDireet• _ I willoVideo_Uploader› _ Vonage© XantDZ00ПZoomBot> ZoomPhoneC) ActivitvcrmFieldsResolver.ohpC) ActivitvLoaService.pho© ActivityProviderClient.php(C) ActivitvProviderRedistrv.oho© ActivityProviderService.php© CallDenormalizerRegistry.phpC) CrmOwnerResolver.oho© DatalmportHandlerInterface.php© MeetingBotService.php© ParticipantConsentService.php© ParticipantsService.phpT PecnonceValidation Trait nhn• SalesforceGetUserTrait.phpSfDenormaliserMainCrmDataTrait.php© TrackRecordingFileSizeService.php© TrackRecordingSizeEnforcer.php© ValidateEmitProspectEventTrait.phpD AjReportsAvatarD CalendarConference0 Crm> D Bullhornv closeOooortunitvsvncStrateav• ProcessonProspectSearchStrateav• M TranslatorC) Client.oho© CloseException.phpC) FieldDefinitions.onvc) SieldValueConverter ohn•) Service nhn© StandardFieldMetadata.php> MConnen© CoreUserRequest.phpscimProvistoning.ong© CoreUser.php=custom.logA console [STAGING]<?phpE laravel.log4 SF [jiminny@localhost]© CoachingFeedbackCoachUserin.phpxA HS_Jocal [jiminny@localhost]A console [PROD]# console [euy© Crm/../Service.php x)declare(scrict_cypes=l)nclass Service extends BaseService 1mpLementsE| A8 A39 X5 A Y169 @* >184 0189 0 >194 0213222 G242namespace Jiminny\Component\ActivitySearch\FilterDefinition;* arnherxcdocfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterfacepublic function getDefaultActivityField(string $activityType): Fieldf….,1 usageorivate const int No GROUP 10 = 999* oinnerztdoeService.phporivate UserRepository,public function getsupportedPlavbooklvpeso: arnav-..."public function __constrpublic function getbealinsightsFields: arrayl...orotected function cetdefault-olLowuolavout-telds/strino sactivitvivoe: arravs...?29 đ >public function shouldApoublic function svncsieldso: void34 đ >public function getQueriSthis->svncStandardFieldso$this->syncCustomFields();45 @t >- 62public function toArray(63 ›-118private function getOptipubLic tunction getvalue* @important Works only for custom fieldspublic function syncField(Field $field): void$resource = $this->convertObjectTypeToResource($field->get0bjectType());// We can only sync custom fields in this CRM.if ($this->isCustomField($field->getCrmProviderId()) === false) {return:=230-131 >=135=136 @›146 €)—150151 G>private function getDefapublic function getValidpublic function getSortooublic function shouldßelInherited members (8R)Anonymous Classes (28))Lambdas (&L)m) & parseObiectType(obiectid: strina): strina 1SupportsObiectTvpeParselnterface® á prepare ValueForUpdate(params: array): array 1 BaseService(m) ở saveActivitvlactivitv: Activitv): Activity TR• d saveFollowup Activity (activity: Activity, fields: array): nullstring 1 RemoteEntityManipula(m) & setcontiduration contio: Contiduration: void "Settinasinterace® ở syncAccount(crmld: string): Account|null + SyncCrmEntitiesinterfacem d syncAccounts(since: \Carbon\Carbon, [to: (Carbon\Carbon null = nullj): int T SyncCrme® d syncContact(rrmld: string): Contact|/null + SyncCrmEntitieslnterfacem d syncContacts(since: (Carbon\Carbon, [to: \Carbon\Carbon null = nullj): int T SyncCrmE@ a syncCustomFields(): voidm d syncField(field: Field): void 1 SyncCrmEntitiesinterface@ & syncFields(): void 1 SyncCrmEntitiesinterfacem d syncLead(crmld: string): Lead null 1 SyncCrmEntitiesinterface® ư syncLeads (since: \Carbon\Carbon, Ito: (Carbon|Carbon/null = null), EcrmProfileld: nulllst@ ơ syncOpportunities(parameters: array, [strategy: null|string = nulll): int 1 SyncCrmEntitie® ở syncOpportunity(crmmld: string): Opportunity|null + SyncCrmEntitiesinterfacem d syncOrganization(): void 1 SyncCrmMetadatalnterface® & syncProfileFields(): void 1 SyncCrmMetadatalnterface@ d syncProfiles([userToSearch: User|null = nulll): Profile|null + SyncCrmMetadatalnterfac® a syncStandardFieds(): void@ e updateRecord(objectType: string, objectid: string, data: array, [headers: array = [...]): vScrnField = Sthis-»getCLient ()-›fetchCustomFdeldDefinition(Sresource, Sfield-sgetCrmProviderTd 156sthas->metadata?rocessor->svncpteld/scrmFleLd):orivate function iscustomsieldstrino Sfieldid: boo-...* @inheritdocpublic function importPicklistValues(Field $field): array{...}( [Platform] Refinemen.... 45 m leftU AskJiminnyReportActivityServiceTest~100% 1Mon 18 May 16:15:20CascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSCIM Role Manageme+0 ..php on LineWARN Metadata found iin doc-comment for methodw9 / 10 tasks done :• Write unit tests for SCIM role management® Contact Mario to update customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META_ROLES and USER_PATCH_ATTR_ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabledl•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintegration.8 Hes with chingon rapp/Component/SClM/ Constants.php +3app/Component/SCIM/ ScimProvisioning.php +85-15nse/D CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/lcer/M RoleAttr.ohn t17tes/Userl ẞ RoleAttrTest ohn +224Accent alliAsk anvthina (&4-L)« Code SWF-1.6WN Windsurf Teamo233:40 UTF-8f 4 spaces...
|
NULL
|
-9186017849702448592
|
NULL
|
click
|
ocr
|
NULL
|
PhostormFV faVsco.js vProjectVIeWg9 master kNaviga PhostormFV faVsco.js vProjectVIeWg9 master kNavigateCodeLaravelKeractorRinaCentralVideoO SalesforceIm Salesloft> D Talkdeskureams> D Telus> D Twilio_ IWillorlex_ I willorlexDireet• _ I willoVideo_Uploader› _ Vonage© XantDZ00ПZoomBot> ZoomPhoneC) ActivitvcrmFieldsResolver.ohpC) ActivitvLoaService.pho© ActivityProviderClient.php(C) ActivitvProviderRedistrv.oho© ActivityProviderService.php© CallDenormalizerRegistry.phpC) CrmOwnerResolver.oho© DatalmportHandlerInterface.php© MeetingBotService.php© ParticipantConsentService.php© ParticipantsService.phpT PecnonceValidation Trait nhn• SalesforceGetUserTrait.phpSfDenormaliserMainCrmDataTrait.php© TrackRecordingFileSizeService.php© TrackRecordingSizeEnforcer.php© ValidateEmitProspectEventTrait.phpD AjReportsAvatarD CalendarConference0 Crm> D Bullhornv closeOooortunitvsvncStrateav• ProcessonProspectSearchStrateav• M TranslatorC) Client.oho© CloseException.phpC) FieldDefinitions.onvc) SieldValueConverter ohn•) Service nhn© StandardFieldMetadata.php> MConnen© CoreUserRequest.phpscimProvistoning.ong© CoreUser.php=custom.logA console [STAGING]<?phpE laravel.log4 SF [jiminny@localhost]© CoachingFeedbackCoachUserin.phpxA HS_Jocal [jiminny@localhost]A console [PROD]# console [euy© Crm/../Service.php x)declare(scrict_cypes=l)nclass Service extends BaseService 1mpLementsE| A8 A39 X5 A Y169 @* >184 0189 0 >194 0213222 G242namespace Jiminny\Component\ActivitySearch\FilterDefinition;* arnherxcdocfinal class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterfacepublic function getDefaultActivityField(string $activityType): Fieldf….,1 usageorivate const int No GROUP 10 = 999* oinnerztdoeService.phporivate UserRepository,public function getsupportedPlavbooklvpeso: arnav-..."public function __constrpublic function getbealinsightsFields: arrayl...orotected function cetdefault-olLowuolavout-telds/strino sactivitvivoe: arravs...?29 đ >public function shouldApoublic function svncsieldso: void34 đ >public function getQueriSthis->svncStandardFieldso$this->syncCustomFields();45 @t >- 62public function toArray(63 ›-118private function getOptipubLic tunction getvalue* @important Works only for custom fieldspublic function syncField(Field $field): void$resource = $this->convertObjectTypeToResource($field->get0bjectType());// We can only sync custom fields in this CRM.if ($this->isCustomField($field->getCrmProviderId()) === false) {return:=230-131 >=135=136 @›146 €)—150151 G>private function getDefapublic function getValidpublic function getSortooublic function shouldßelInherited members (8R)Anonymous Classes (28))Lambdas (&L)m) & parseObiectType(obiectid: strina): strina 1SupportsObiectTvpeParselnterface® á prepare ValueForUpdate(params: array): array 1 BaseService(m) ở saveActivitvlactivitv: Activitv): Activity TR• d saveFollowup Activity (activity: Activity, fields: array): nullstring 1 RemoteEntityManipula(m) & setcontiduration contio: Contiduration: void "Settinasinterace® ở syncAccount(crmld: string): Account|null + SyncCrmEntitiesinterfacem d syncAccounts(since: \Carbon\Carbon, [to: (Carbon\Carbon null = nullj): int T SyncCrme® d syncContact(rrmld: string): Contact|/null + SyncCrmEntitieslnterfacem d syncContacts(since: (Carbon\Carbon, [to: \Carbon\Carbon null = nullj): int T SyncCrmE@ a syncCustomFields(): voidm d syncField(field: Field): void 1 SyncCrmEntitiesinterface@ & syncFields(): void 1 SyncCrmEntitiesinterfacem d syncLead(crmld: string): Lead null 1 SyncCrmEntitiesinterface® ư syncLeads (since: \Carbon\Carbon, Ito: (Carbon|Carbon/null = null), EcrmProfileld: nulllst@ ơ syncOpportunities(parameters: array, [strategy: null|string = nulll): int 1 SyncCrmEntitie® ở syncOpportunity(crmmld: string): Opportunity|null + SyncCrmEntitiesinterfacem d syncOrganization(): void 1 SyncCrmMetadatalnterface® & syncProfileFields(): void 1 SyncCrmMetadatalnterface@ d syncProfiles([userToSearch: User|null = nulll): Profile|null + SyncCrmMetadatalnterfac® a syncStandardFieds(): void@ e updateRecord(objectType: string, objectid: string, data: array, [headers: array = [...]): vScrnField = Sthis-»getCLient ()-›fetchCustomFdeldDefinition(Sresource, Sfield-sgetCrmProviderTd 156sthas->metadata?rocessor->svncpteld/scrmFleLd):orivate function iscustomsieldstrino Sfieldid: boo-...* @inheritdocpublic function importPicklistValues(Field $field): array{...}( [Platform] Refinemen.... 45 m leftU AskJiminnyReportActivityServiceTest~100% 1Mon 18 May 16:15:20CascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSCIM Role Manageme+0 ..php on LineWARN Metadata found iin doc-comment for methodw9 / 10 tasks done :• Write unit tests for SCIM role management® Contact Mario to update customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META_ROLES and USER_PATCH_ATTR_ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabledl•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintegration.8 Hes with chingon rapp/Component/SClM/ Constants.php +3app/Component/SCIM/ ScimProvisioning.php +85-15nse/D CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/lcer/M RoleAttr.ohn t17tes/Userl ẞ RoleAttrTest ohn +224Accent alliAsk anvthina (&4-L)« Code SWF-1.6WN Windsurf Teamo233:40 UTF-8f 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
54550
|
1896
|
69
|
2026-05-18T13:15:21.019543+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110121019_m1.jpg...
|
PhpStorm
|
faVsco.js – Crm/…/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp:[Platform) Refinemen….45 m leftmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com8• Mon 18 May 16:15:20Nikolay Yankov (Presenting)Mon 18 May 16:16$(18® For you© Recent|# Starred8? Apps07 SpacesSaarco59 Service-DeskJiminny (Oid)Jiminny (New)1 CID Platform TeamCID Capture Team|IID Enterprise Stabllity L.(ID Processing TeamIID SE Kanban|= More spaces= FiltersCB Dashboards6 Operations83 Confluence2: Teamd% Customise sidebarC Ate© ours* CiaoosapoaC Alo3 Ppe0 3mi© 3mi2 SSH• Home | SalestorceDatadog* Claude*xISpaces Jminny (New)Add parent#JY-20679Enable users to use their new activity types• DescriptionCurrently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomacons that we have - this means they need to copy ar or their prompes egan. wnich takes a nuge amourk or üreFor SF, Hubspot, Copper, Bullhorn, Close and Pipedrive (anything apart from Zoho)• when a new activity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to all the playbooks where activity type field ischeneru• if a customer manualy creates an activity type in Jiminny - show them a warning message on the top saying "If you want this activity type to sync to your CRM oneach call - make sure to have it in your CRM with lne exact same name*" - use this https://www.figma.com/design/XcUety9mx5Fiz8KosLAUn/Project-Phoenix2node-id=7483-2600188t-uJOZVbpzYIpC5PAy-1 [Connect your Floma ascount• the message should stay only while they are adding the activity type - il they leave or refresh the page it should disappear. Use the info message that we haveSubtasksAdd subtaskLinked work itemsAdd linked work item~ ActivityWork logRehconmeneSuggest a reply.….Who is working on this…..?Status update.Pro tip: press M to commer1 шr-Z tY-* ProlInsights & CoachinC DevAsk RovoBackiogyIX Improve Story~ DetailsAssignee8 UnassignedAssign to meKUpOrtE& Galya Dimitrova• Quick start developmentLink this work item to your code byincluding keys when creatingabranch, commit, or pull requestDismissycoemher@ Open with VS CodeCreate branch* Create commit)ComponentsPlatformSo-rtoNoneStory point estimatoNikolay YankovNikolay IvanovAneliya AngelovaGalya Dimitrova4:15 PM | [Platform] Refinement ®Lukas Kovalik1:01:48...
|
NULL
|
-1854099937965702864
|
NULL
|
click
|
ocr
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfilesToolsWi FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp:[Platform) Refinemen….45 m leftmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com8• Mon 18 May 16:15:20Nikolay Yankov (Presenting)Mon 18 May 16:16$(18® For you© Recent|# Starred8? Apps07 SpacesSaarco59 Service-DeskJiminny (Oid)Jiminny (New)1 CID Platform TeamCID Capture Team|IID Enterprise Stabllity L.(ID Processing TeamIID SE Kanban|= More spaces= FiltersCB Dashboards6 Operations83 Confluence2: Teamd% Customise sidebarC Ate© ours* CiaoosapoaC Alo3 Ppe0 3mi© 3mi2 SSH• Home | SalestorceDatadog* Claude*xISpaces Jminny (New)Add parent#JY-20679Enable users to use their new activity types• DescriptionCurrently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomacons that we have - this means they need to copy ar or their prompes egan. wnich takes a nuge amourk or üreFor SF, Hubspot, Copper, Bullhorn, Close and Pipedrive (anything apart from Zoho)• when a new activity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to all the playbooks where activity type field ischeneru• if a customer manualy creates an activity type in Jiminny - show them a warning message on the top saying "If you want this activity type to sync to your CRM oneach call - make sure to have it in your CRM with lne exact same name*" - use this https://www.figma.com/design/XcUety9mx5Fiz8KosLAUn/Project-Phoenix2node-id=7483-2600188t-uJOZVbpzYIpC5PAy-1 [Connect your Floma ascount• the message should stay only while they are adding the activity type - il they leave or refresh the page it should disappear. Use the info message that we haveSubtasksAdd subtaskLinked work itemsAdd linked work item~ ActivityWork logRehconmeneSuggest a reply.….Who is working on this…..?Status update.Pro tip: press M to commer1 шr-Z tY-* ProlInsights & CoachinC DevAsk RovoBackiogyIX Improve Story~ DetailsAssignee8 UnassignedAssign to meKUpOrtE& Galya Dimitrova• Quick start developmentLink this work item to your code byincluding keys when creatingabranch, commit, or pull requestDismissycoemher@ Open with VS CodeCreate branch* Create commit)ComponentsPlatformSo-rtoNoneStory point estimatoNikolay YankovNikolay IvanovAneliya AngelovaGalya Dimitrova4:15 PM | [Platform] Refinement ®Lukas Kovalik1:01:48...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
54552
|
1897
|
38
|
2026-05-18T13:15:28.244607+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110128244_m2.jpg...
|
PhpStorm
|
faVsco.js – Crm/…/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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}]...
|
8101791824998039176
|
-8348547798392525882
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
PhostormVIewINavicarecodeLaravelKeractorFV faVsco.js°9 master kProiectRinaCentralVideo• SalesforceIm Salesloft> D TalkdeskDTeams> D Telus>D Twilio>@ TwilioFlex_ I willorlexDireet• _ I willoVideo_Uploader› _ Vonagewxant>400mZoomBot> ZoomPhoneC) ActivitvcrmFieldsResolver.ohpC) ActivitvLoaService.oho© ActivitvProviderClient.ohp(C) ActivitvProviderRedistrv.oho© ActivityProviderService.php© CallDenormalizerRegistry.phpC) [EMAIL]© DatalmportHandlerInterface.php© MeetingBotService.php© ParticipantConsentService.php(c) DarticinanteService nhnT PecnonceValidation Trait nhnT SalesforceGetUserTrait.php- S/DenormaliserMainCrmDatalrait.onp@ TrackRecordinoFllesizeservice.one©1rаcкkecoraingsizetntorcer.onpT ValidateEmitProspectEventTrait.phpC AjReports0 AvatarMColondar0 Conference0 Crm> C Bullhornv closeOnoortunitvsvncstrateav• ProcessonProspectSearchStrateav• M TranslatorC) Client.oho() CloseSxceotion.ohoC) FieldDefinitions.onvc) SieldValueConverter ohnC) Service nhnC) StandardFioldMetadata nhn> MConnen© SoftPhoneManager.phpC) CoreUserRequest.onp© CoreUser.php©Crm/…../Service.php Xclass Service extends BaseService 1mpLementsm A8 A39 M5 лV1 0 >132 01141 0г )150 C169 €189 Gu5194015213 €oubl1c tunction normauzevaluelstrino rleldivoe, strino rieldvalue, bool sinternal = tause): strorotected function getfieldivoeso: arrav....?orotected function getfields(strina Scrmobiect): arrav-...?* Oinheritdocpublic function getDefaultFields(string $activityType): array{...}* @inheritdocpublic function getDefaultActivityField(string SactivityType): Field// Set up the activity field as the default Type/** @var Field SactivituField */SactivityField = Sthis->confiq->fields->where(i'obnect type' =› sactivitylype,1)->first@:return Sactivitvfield:Oinheritdocnublic function aetSunnortedPlavhookTvnes@• arrav-...?public function getDealInsightsFields: array{...}protected function getDefaultFollowupLayoutFields(string $activityType): array{...}public function syncFields(): void$this->syncStandardFieldsO:$this->syncCustomFieldsO:* Cimportant Works only for custom fields= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING]© CoachingFeedbackCoachUserln.php Xstohedeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 Ф >34 đ >45 O>— 139'136 đt ›_ 146 Gt ›-150151 G>1usadeorivate const int No GROUP 10 = 9993 usagesorivate UserRenository suserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection...hpublic function toArray: array{...hprivate function getOptions(): array{...}public function getValue: array{...}private function getDefaultValue: array{...}public function aetValidationRules(2strina Sorefix = null): arrav{...?lpublic function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}: [Platform] Refinemen... 45 m left100% 1• Mon 18 May 16:15:28AskJiminnyReportActivityServiceTest vCascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSCIM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for methodw9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ATTR_ ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations.4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabled•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/Ueer/M PoleAttr.nhn +17-ites/User/ ẞ RoleAttrTest.nhn +224* Reiect alliiAccent alliAsk anvthina (&4-L)« Code SWF-1.6WN Windsurf Toams 160-21UTF.8io 4 spaces...
|
54551
|
NULL
|
NULL
|
NULL
|
|
54553
|
1896
|
70
|
2026-05-18T13:15:28.459450+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110128459_m1.jpg...
|
PhpStorm
|
faVsco.js – Crm/…/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-943456739835505562
|
-8636494053633454656
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp:[Platform) Refinemen….45 m leftmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com8• Mon 18 May 16:15:28Nikolay Yankov (Presenting)Mon 18 May 16:16(18® For you© Recent|# Starred8? Apps07 Spacesauarco(9 Service-DeskJiminny (Old)Jiminny (New)+--I CD Platform TeamCID Capture Team |IID Enterprise Stabllity L.(ID Processing TeamIID SE Kanban|= More spaces= FiltersB Dashboards6 OperationsE3 Confluence2: Teamd* Customise sidebarC Atte© Ours* CiauCost aooaC Alo3 Pipe© Jmk© mhttps://fminny.atlassian.net/browse/JY-20879E3 SSH• Home | Salestorce008 Dt ©* Claudel*xISpacesJminny (New)Add parent#JY-20679Enable users to use their new activity types• DescriptionCurrently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomacons that we have - this means they need to copy an or their prompes agan. wnich takes a nuge amourk or üreFor SF, Hubspot, Copper, Bullhorn, Close and Pipedrive (anything apart from Zoho)• when a new activity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to all the playbooks where activity type field ischeteru• if a customer manualy creates an activity type in Jiminny - show them a warning message on the top saying "If you want this activity type to sync to your CRM oneach call - make sure to have i& in your CRM with the exact same name* - use this F https://www.figma.com/design/XcUety9mxSFiz8KosLAUn/Project-Phoenb?node-id=7483-2600188t-uJOZVbpzYIpC5PAy-1 [Connect your Fioma ascount• the message should stay only while they are adding the activity type - Il they leave or refresh the page it should disappear. Use the info message that we haveSubtasksAdd subtaskLinked work itemsAdd linked work item~ ActivityWork logSuggest a reply....Who is working on this…..?Status update.Pro tip: press (M to commenZ (Y-* Prolinsights & CoachinC Dev- UXAsk RovoBackiogII Improve StoryDetailsAssignee8 UnassignedAssign to meKUpOrtE& Galya Dimitrova• Quick start developmentLink this work item to your code byincluding keys when creatingabranch, commit, or pull requestDismisseycoemherQ Open with VS CodeCreate branch* Create commit)ComponentsPlatformNorkStory point estimatoNikolay YankovNikolay IvanovAneliya AngelovaGalya Dimitrova4:15 PM | [Platform] Refinement ®Lukas Kovalik1:01:55...
|
54550
|
NULL
|
NULL
|
NULL
|
|
54554
|
1897
|
39
|
2026-05-18T13:15:30.949614+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110130949_m2.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavicarecodeLaravelKeractorFV faVsco. PhostormVIewINavicarecodeLaravelKeractorFV faVsco.js°9 master kroledeyRinaCentralVideo• SalesforceIm Salesloft> D Talkdeskureams> D Telus>D Twilio>@ TwilioFlex_ I willorlexDireet• _ I willoVideo_Uploader› _ Vonagewxant>400mZoomBot> ZoomPhoneC) ActivitvcrmFieldsResolver.ohpC) ActivitvLoaService.oho© ActivitvProviderClient.ohp(C) ActivitvProviderRedistrv.oho© ActivityProviderService.php© CallDenormalizerRegistry.phpC) [EMAIL]© DatalmportHandlerInterface.php© MeetingBotService.php© ParticipantConsentService.php(c) DarticinanteService nhnT PecnonceValidation Trait nhnT SalesforceGetUserTrait.php- S/DenormaliserMainCrmDatalrait.onp@ TrackRecordinoFllesizeservice.one©1rаcкkecoraingsizetntorcer.onpT ValidateEmitProspectEventTrait.phpC AjReports0 AvatarMColondar0 Conference0 Crm> 0 Bullhornv closeOnoortunitvsvncstrateav• ProcessonProspectSearchStrateav• M TranslatorC) Client.ohr() CloseSxceotion.ohoC) FieldDefinitions.onvc) SieldValueConverter ohn•) Service nhnC) StandardFieldMetadata nhn> MConnen© SoftPhoneManager.php(C) CoreUserRequest.onp© CoreUser.php©Crm/…../Service.php Xclass Service extends BaseService 1mpLementsm A8 A39 M5 лV1 0 >132 01141 0г )15A (169189 Gu5194015213 €oubl1c tunction normauzevaluelstrino rleldivoe, strino rieldvalue, bool sinternal = tause): strorotected function getfieldivoeso: arrav....?orotected function getfields(strina Scrmobiect): arrav-...?* Oinheritdocpublic function getDefaultFields(string $activityType): array{...}* @inheritdocpublic function getDefaultActivityField(string SactivityType): Field// Set up the activity field as the default Type/** @var Field SactivituField */SactivityField = Sthis->confiq->fields->where(['crm provider id' => 'tvpe'.'obnect type' =› sactivitylype,1)->first@:return Sactivitvfield:Oinheritdocnublic function aetSunnortedPlavhookTvnes@• arrav-...?public function getDealInsightsFields: array{...}protected function getDefaultFollowupLayoutFields(string $activityType): array{...}public function syncFields(): void$this->syncStandardFieldsO:$this->syncCustomFieldsO:* Cimportant Works only for custom fields= custom.log= laravel.log« SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING]© CoachingFeedbackCoachUserln.php Xstohedeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 >34 đ >45 >—119÷131'136 đt ›_ 146 Gt ›-150151 G>1usadeorivate const int No GROUP 10 = 9993 usagesorivate UserRenository suserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection...hpublic function toArray: array{...hprivate function getOptions(): array{...}public function getValue: array{...}private function getDefaultValue: array{...}public function aetValidationRules(2strina Sorefix = null): arrav{...?lpublic function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}: [Platform] Refinemen... 45 m left100% 1• Mon 18 May 16:15:30AskJiminnyReportActivityServiceTest vCascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSCIM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for methodw9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ATTR_ ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations.4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabled•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/Ueer/M PoleAttr.nhn +17-ites/User/ ẞ RoleAttrTest.nhn +224* Reiect alliiAccent alliAsk anvthina (&4-L)« Code SWF-1.6WN Windsurf Toams 171-26 UTF.8io 4 spaces...
|
NULL
|
-4755028410154996494
|
NULL
|
click
|
ocr
|
NULL
|
PhostormVIewINavicarecodeLaravelKeractorFV faVsco. PhostormVIewINavicarecodeLaravelKeractorFV faVsco.js°9 master kroledeyRinaCentralVideo• SalesforceIm Salesloft> D Talkdeskureams> D Telus>D Twilio>@ TwilioFlex_ I willorlexDireet• _ I willoVideo_Uploader› _ Vonagewxant>400mZoomBot> ZoomPhoneC) ActivitvcrmFieldsResolver.ohpC) ActivitvLoaService.oho© ActivitvProviderClient.ohp(C) ActivitvProviderRedistrv.oho© ActivityProviderService.php© CallDenormalizerRegistry.phpC) [EMAIL]© DatalmportHandlerInterface.php© MeetingBotService.php© ParticipantConsentService.php(c) DarticinanteService nhnT PecnonceValidation Trait nhnT SalesforceGetUserTrait.php- S/DenormaliserMainCrmDatalrait.onp@ TrackRecordinoFllesizeservice.one©1rаcкkecoraingsizetntorcer.onpT ValidateEmitProspectEventTrait.phpC AjReports0 AvatarMColondar0 Conference0 Crm> 0 Bullhornv closeOnoortunitvsvncstrateav• ProcessonProspectSearchStrateav• M TranslatorC) Client.ohr() CloseSxceotion.ohoC) FieldDefinitions.onvc) SieldValueConverter ohn•) Service nhnC) StandardFieldMetadata nhn> MConnen© SoftPhoneManager.php(C) CoreUserRequest.onp© CoreUser.php©Crm/…../Service.php Xclass Service extends BaseService 1mpLementsm A8 A39 M5 лV1 0 >132 01141 0г )15A (169189 Gu5194015213 €oubl1c tunction normauzevaluelstrino rleldivoe, strino rieldvalue, bool sinternal = tause): strorotected function getfieldivoeso: arrav....?orotected function getfields(strina Scrmobiect): arrav-...?* Oinheritdocpublic function getDefaultFields(string $activityType): array{...}* @inheritdocpublic function getDefaultActivityField(string SactivityType): Field// Set up the activity field as the default Type/** @var Field SactivituField */SactivityField = Sthis->confiq->fields->where(['crm provider id' => 'tvpe'.'obnect type' =› sactivitylype,1)->first@:return Sactivitvfield:Oinheritdocnublic function aetSunnortedPlavhookTvnes@• arrav-...?public function getDealInsightsFields: array{...}protected function getDefaultFollowupLayoutFields(string $activityType): array{...}public function syncFields(): void$this->syncStandardFieldsO:$this->syncCustomFieldsO:* Cimportant Works only for custom fields= custom.log= laravel.log« SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]# console [euyA console [STAGING]© CoachingFeedbackCoachUserln.php Xstohedeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 >34 đ >45 >—119÷131'136 đt ›_ 146 Gt ›-150151 G>1usadeorivate const int No GROUP 10 = 9993 usagesorivate UserRenository suserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection...hpublic function toArray: array{...hprivate function getOptions(): array{...}public function getValue: array{...}private function getDefaultValue: array{...}public function aetValidationRules(2strina Sorefix = null): arrav{...?lpublic function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}: [Platform] Refinemen... 45 m left100% 1• Mon 18 May 16:15:30AskJiminnyReportActivityServiceTest vCascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSCIM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for methodw9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ATTR_ ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations.4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabled•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/Ueer/M PoleAttr.nhn +17-ites/User/ ẞ RoleAttrTest.nhn +224* Reiect alliiAccent alliAsk anvthina (&4-L)« Code SWF-1.6WN Windsurf Toams 171-26 UTF.8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
54555
|
1896
|
71
|
2026-05-18T13:15:32.147693+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110132147_m1.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - [Platform] Refinement 🔍","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
7826844677319556675
|
7235299109264185591
|
click
|
hybrid
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
F Meet - [Platform] Refinement 🔍
Close tab
New Tab
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp§ [Platform] Refinemen... 45 m left100% C8• Mon 18 May 16:15:31meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com=Nikolay Yankov (Presenting)+Mon 18 May 16:16(18® For you© Recent|# Starred8? Apps07 Spacesauarco(9 Service-DeskJiminny (Old)Jiminny (New)+--I CD Platform TeamCID Capture Team|IID Enterprise Stabllity L.(ID Processing TeamIID SE Kanban|= More spaces= FiltersB Dashboards® OperationsE3 Confluence2: Teamd* Customise sidebarC Atte© Ours* 0-%Costaop.C Aloi3 Pipe0 3m0 smlo JimE3 SSH• Home | SalestorceE Datadog* Claudel*xISpacesJminny (New)Add parent#JY-20679Enable users to use their new activity types• DescriptionCurrently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomacons that we have - this means they need to copy an or their prompes egan. wnich takes a nuge amourk or üroeFor SF, Hubspot, Copper, Bullhorn, Close and Pipedrive (anything apart from Zoho)• when a new activity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to all the playbooks where activity type field ischeteru• if a customer manualy creates an activity type in Jiminny - show them a warning message on the top saying "If you want this activity type to sync to your CRM oneach call - make sure to have i& in your CRM with the exact same name* - use this F https://www.figma.com/design/XcUety9mxSFiz8KosLAUn/Project-Phoenb?node-id=7483-2600188t-uJOZVbpzYIpC5PAy-1 [Connect your Fioma account• the message should stay only while they are adding the activity type - Il they leave or refresh the page it should disappear. Use the info message that we haveSubtasksAdd subtaskLinked work itemsAdd linked work item~ ActivityWork logSuggest a reply.…Who is working on this...?Status update…Pro tip: press M to commenZ (Y-* Prolinsights & CoachinC Dev- UXAsk RovoBackiogII Improve Story~ DetailsAssignee8 UnassignedAssign to meKUpOrtE& Galya Dimitrova• Quick start developmentLink this work item to your code byincluding keys when creatingabranch, commit, or pull requestbelow. Learn moreDismisseycoemerQ Open with VS CodeCreate branch* Create commit)ComponentsPlatformNoneStory point estimatoNikolay YankovNikolay Ivanov000Aneliya AngelovaSalya Dimitrova4:15 PM | [Platform] Refinement ®Lukas Kovalik1:01:59...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
54556
|
NULL
|
0
|
2026-05-18T13:15:40.015889+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110140015_m1.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
6
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Unpin Nikolay Yankov's presentation from your main screen
You can't unmute someone else's presentation
More options for Nikolay Yankov
Zoom in
Open in new window
Enter Full Screen
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
Pin Nikolay Ivanov to your main screen
You can't unmute someone else
More options for Nikolay Ivanov...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - [Platform] Refinement 🔍","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.8411111,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.0,"top":0.8794444,"width":0.033680554,"height":0.03888889},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.0,"top":0.91833335,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.95666665,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Nikolay Yankov (Presenting)","depth":12,"bounds":{"left":0.07534722,"top":0.101111114,"width":0.124305554,"height":0.022222223},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Yankov (Presenting)","depth":13,"bounds":{"left":0.07534722,"top":0.10222222,"width":0.124305554,"height":0.020555556},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.88680553,"top":0.08944444,"width":0.04097222,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":22,"bounds":{"left":0.9145833,"top":0.101111114,"width":0.0048611113,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Take notes with Gemini","depth":14,"bounds":{"left":0.93333334,"top":0.08944444,"width":0.025,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Take notes with Gemini","depth":17,"bounds":{"left":0.9361111,"top":0.101111114,"width":0.06388891,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Gemini","depth":17,"bounds":{"left":0.96666664,"top":0.101111114,"width":0.028125,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":17,"bounds":{"left":0.96458334,"top":0.090555556,"width":0.023611112,"height":0.037777778},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Unpin Nikolay Yankov's presentation from your main screen","depth":13,"bounds":{"left":0.34618056,"top":0.5088889,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else's presentation","depth":13,"bounds":{"left":0.37395832,"top":0.50666666,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Yankov","depth":13,"bounds":{"left":0.4045139,"top":0.5088889,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Zoom in","depth":13,"bounds":{"left":0.63090277,"top":0.78333336,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open in new window","depth":13,"bounds":{"left":0.6642361,"top":0.78333336,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Enter Full Screen","depth":13,"bounds":{"left":0.69756943,"top":0.78333336,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Pin Nikolay Yankov to your main screen","depth":13,"bounds":{"left":0.7607639,"top":0.25111112,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Mute Nikolay Yankov's microphone","depth":13,"bounds":{"left":0.7885417,"top":0.2488889,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Yankov","depth":13,"bounds":{"left":0.8190972,"top":0.25111112,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Nikolay Yankov","depth":17,"bounds":{"left":0.753125,"top":0.36277777,"width":0.07673611,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Nikolay Ivanov to your main screen","depth":13,"bounds":{"left":0.8871528,"top":0.25111112,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else","depth":13,"bounds":{"left":0.9149306,"top":0.2488889,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Ivanov","depth":13,"bounds":{"left":0.9454861,"top":0.25111112,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-3633716926313692317
|
-2882179155567322094
|
click
|
hybrid
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
6
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Unpin Nikolay Yankov's presentation from your main screen
You can't unmute someone else's presentation
More options for Nikolay Yankov
Zoom in
Open in new window
Enter Full Screen
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
Pin Nikolay Ivanov to your main screen
You can't unmute someone else
More options for Nikolay Ivanov
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp> 0.§ [Platform] Refinemen... 45 m left100% C8• Mon 18 May 16:15:39meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com=Nikolay Yankov (Presenting)+Mon 18 May 16:16(18® For you© Recent|# Starred8? Apps0 Spacesacarco(9 Service-DeskJiminny (Old)Jiminny (New)I CD Platform TeamCID Capture Team|IID Enterprise Stabllity L.(ID Processing TeamIID SE Kanban|= More spaces= FiltersB Dashboards6 OperationsE3 Confluence38 Team-* Customise sidebarC Atte© Ours* 0%CostaopC AlOIPpe0 3m0 smloJimla tY-E3 SSH• Home | SalestorceE Datadog*xISpacesJminny (New)C. Add parem#JY-20679Enable users to use their new activity types• DescriptionCurrently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomacons that we have - this means they need to copy a or their prompes egan. wnich takes a nuge amourk or troeFor SF, Hubspot, Copper, Bullhorn, Close and Pipedrive (anything apart from Zoho)• when a new activity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to all the playbooks where activity type field ischeter• if a customer manualy creates an activity type in Jiminny - show them a warning message on the top saying "If you want this activity type to sync to your CRM oneach call - make sure to have it in your CRM with the exact same name* - use this F https://www.figma.com/design/XcUety9mxSFiz8KosLAUn/Project-Phoenb?node-id=7483-2600188t-uJOZVbpzYIpC5PAy-1 [Connect your Fioma account• the message should stay only while they are adding the activity type - Il they leave or refresh the page it should disappear. Use the info message that we haveSubtasksAdd subtaskLinked work itemsAdd linked work item~ ActivityWork logSuggest a reply.…..Who is working on this…..?Status update…Pro tip: press M to commenZ UY-* Prolinsights & CoachinC DevAsk RovoBackiogIX Improve StoryDetailsAssignee8 UnassignedAssign to meKUpOrtE& Galya Dimitrova• Quick start developmentLink this work item to your code byincluding keys when creatingabranch, commit, or pull request|DismissQ Open with VS CodeCreate branch* Create commit)ComponentsPlatformNoneStory point estimatoNikolay YankovNikolay IvanoyAneliya AngelovaGalya Dimitrova4:15 PM | [Platform] Refinement ®Lukas Kovalik1:02:07Ill...
|
54555
|
NULL
|
NULL
|
NULL
|
|
54557
|
NULL
|
0
|
2026-05-18T13:15:40.055162+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110140055_m2.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
6
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Unpin Nikolay Yankov's presentation from your main screen
You can't unmute someone else's presentation
More options for Nikolay Yankov
Zoom in
Open in new window
Enter Full Screen
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
Pin Nikolay Ivanov to your main screen
You can't unmute someone else
More options for Nikolay Ivanov
Nikolay Ivanov
Pin Aneliya Angelova to your main screen
You can't unmute someone else
More options for Aneliya Angelova
Aneliya Angelova
Pin Galya Dimitrova to your main screen
Mute Galya Dimitrova's microphone...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - [Platform] Refinement 🔍","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.016123671,"height":-0.051875472},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.27094415,"top":1.0,"width":0.004986702,"height":-0.051875472},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.27310506,"top":1.0,"width":0.010638298,"height":-0.086193085},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Nikolay Yankov (Presenting)","depth":12,"bounds":{"left":0.30634972,"top":1.0,"width":0.059507977,"height":-0.072625756},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Yankov (Presenting)","depth":13,"bounds":{"left":0.30634972,"top":1.0,"width":0.059507977,"height":-0.07342374},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.69481385,"top":1.0,"width":0.019614361,"height":-0.06424582},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":22,"bounds":{"left":0.7081117,"top":1.0,"width":0.0023271276,"height":-0.072625756},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Take notes with Gemini","depth":14,"bounds":{"left":0.71708775,"top":1.0,"width":0.011968086,"height":-0.06424582},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Take notes with Gemini","depth":17,"bounds":{"left":0.7184175,"top":1.0,"width":0.043550532,"height":-0.072625756},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Gemini","depth":17,"bounds":{"left":0.7330452,"top":1.0,"width":0.013464096,"height":-0.072625756},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":17,"bounds":{"left":0.73204786,"top":1.0,"width":0.011303191,"height":-0.065043926},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Unpin Nikolay Yankov's presentation from your main screen","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else's presentation","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Yankov","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Zoom in","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open in new window","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Enter Full Screen","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Pin Nikolay Yankov to your main screen","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Mute Nikolay Yankov's microphone","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Yankov","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Nikolay Yankov","depth":17,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Nikolay Ivanov to your main screen","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Ivanov","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":17,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Aneliya Angelova to your main screen","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Aneliya Angelova","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Aneliya Angelova","depth":17,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Galya Dimitrova to your main screen","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Mute Galya Dimitrova's microphone","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
4944893920354227949
|
-5151641352240374256
|
click
|
hybrid
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
6
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Unpin Nikolay Yankov's presentation from your main screen
You can't unmute someone else's presentation
More options for Nikolay Yankov
Zoom in
Open in new window
Enter Full Screen
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
Pin Nikolay Ivanov to your main screen
You can't unmute someone else
More options for Nikolay Ivanov
Nikolay Ivanov
Pin Aneliya Angelova to your main screen
You can't unmute someone else
More options for Aneliya Angelova
Aneliya Angelova
Pin Galya Dimitrova to your main screen
Mute Galya Dimitrova's microphone
PnostormVIewINavicatecodeFV faVsco.js°9 master kProiectRinaCentralVideoSalesforceIm Salesloft> D TalkdeskD Teams> D Telus)M Twilia>@ TwilioFlex_ I willorlexDireet• _ I willoVideo_Uploader› _ Vonagewxant>400mZoomBot> ZoomPhoneC) ActivitvcrmFieldsResolver.ohpC) ActivitvLoaService.oho© ActivitvProviderClient.ohn(C) ActivitvProviderRedistrv.oho© ActivityProviderService.php© CallDenormalizerRegistry.phpC) CrmOwnerResolver.oho© DatalmportHandlerInterface.php© MeetingBotService.php© ParticipantConsentService.php(c) DarticinanteService nhnT PecnonceValidation Trait nhnT SalesforceGetUserTrait.php- S/DenormaliserMainCrmDatalrait.onp@ TrackRecordinoFllesizeservice.one©1rаcкkecoraingsizetntorcer.onpT ValidateEmitProspectEventTrait.phpC AjReports0 AvatarMColondar0 Conference0 Crm> 0 Bullhornv closeOnoortunitvsvncstrateav• ProcessonProspectSearchStrateav• M TranslatorC) Client.ohr() CloseSxceotion.ohoC) FieldDefinitions.onvc) SieldValueConverter ohnC) Service nhnC) StandardFioldMetadata nhn> MConnen© SoftPhoneManager.phpC) CoreUserRequest.onp© CoreUser.php© Crm/../Service.php xclass Service extends BaseService 1mpLementsm A8 A39 M5 лV1 0 >132 01141 0г )15A (169189 Gu5194015213 Coubl1c tunction normauzevaluelstrino rleldivoe, strino rieldvalue, bool sinternal = tause): strorotected function getfieldivoeso: arrav....?orotected function getfields(strina Scrmobiect): arrav-...?* Oinheritdocpublic function getDefaultFields(string $activityType): array{...}* @inheritdocpublic function getDefaultActivityField(string SactivityType): Field// Set up the activity field as the default Type/** @var Field SactivituField */SactivityField = Sthis->confiq->fields->where(['obnect type' =› sactivitylype,1)->first@:return SactivitvField:Oinheritdocnublic function aetSunnortedPlavhookTvnes@• arrav-...?public function getDealInsightsFields: array{...}protected function getDefaultFollowupLayoutFields(string $activityType): array{...}public function syncFields(): void$this->syncStandardFieldsO:$this->syncCustomFieldsO:* Cimportant Works only for custom fields= custom.log= laravel.log« SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]& console [EU]A console [STAGING]© CoachingFeedbackCoachUserln.php Xstohedeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 Ф >34 đ >45 đ >—119÷131'136 đt ›_ 146 Gt ›-150151 G>usadeorivate const int No GROUP 10 = 9993 usagesorivate UserRenository suserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection...hpublic function toArray: array{...hprivate function getOptions(): array{...}public function getValue: array{...}private function getDefaultValue: array{...}public function aetValidationRules(2strina Sorefix = null): arrav{...?lpublic function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}40m let• мon 10 May 10.10.34cascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSClM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for method9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ATTR_ ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations.4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabled•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/Ueer/M PoleAttr.nhn +17-ites/User/ ẞ RoleAttrTest.nhn +224* Reiect alliiAccent alliAsk anvthina (&4-L)« Code SWF-1.6W Windsurf Toams 171-26UTF.Afo 4 spaces...
|
54554
|
NULL
|
NULL
|
NULL
|
|
54558
|
1898
|
0
|
2026-05-18T13:15:41.183967+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110141183_m1.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
6
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Unpin Nikolay Yankov's presentation from your main screen
You can't unmute someone else's presentation
More options for Nikolay Yankov
Zoom in
Open in new window
Enter Full Screen
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
Pin Nikolay Ivanov to your main screen
You can't unmute someone else
More options for Nikolay Ivanov
Nikolay Ivanov
Pin Aneliya Angelova to your main screen
You can't unmute someone else
More options for Aneliya Angelova...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - [Platform] Refinement 🔍","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.8411111,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.0,"top":0.8794444,"width":0.033680554,"height":0.03888889},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.0,"top":0.91833335,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.95666665,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Nikolay Yankov (Presenting)","depth":12,"bounds":{"left":0.07534722,"top":0.101111114,"width":0.124305554,"height":0.022222223},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Yankov (Presenting)","depth":13,"bounds":{"left":0.07534722,"top":0.10222222,"width":0.124305554,"height":0.020555556},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.88680553,"top":0.08944444,"width":0.04097222,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":22,"bounds":{"left":0.9145833,"top":0.101111114,"width":0.0048611113,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Take notes with Gemini","depth":14,"bounds":{"left":0.93333334,"top":0.08944444,"width":0.025,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Take notes with Gemini","depth":17,"bounds":{"left":0.9361111,"top":0.101111114,"width":0.06388891,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Gemini","depth":17,"bounds":{"left":0.96666664,"top":0.101111114,"width":0.028125,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":17,"bounds":{"left":0.96458334,"top":0.090555556,"width":0.023611112,"height":0.037777778},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Unpin Nikolay Yankov's presentation from your main screen","depth":13,"bounds":{"left":0.34618056,"top":0.5088889,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else's presentation","depth":13,"bounds":{"left":0.37395832,"top":0.50666666,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Yankov","depth":13,"bounds":{"left":0.4045139,"top":0.5088889,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Zoom in","depth":13,"bounds":{"left":0.63090277,"top":0.78333336,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open in new window","depth":13,"bounds":{"left":0.6642361,"top":0.78333336,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Enter Full Screen","depth":13,"bounds":{"left":0.69756943,"top":0.78333336,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Pin Nikolay Yankov to your main screen","depth":13,"bounds":{"left":0.7607639,"top":0.25111112,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Mute Nikolay Yankov's microphone","depth":13,"bounds":{"left":0.7885417,"top":0.2488889,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Yankov","depth":13,"bounds":{"left":0.8190972,"top":0.25111112,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Nikolay Yankov","depth":17,"bounds":{"left":0.753125,"top":0.36277777,"width":0.07673611,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Nikolay Ivanov to your main screen","depth":13,"bounds":{"left":0.8871528,"top":0.25111112,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else","depth":13,"bounds":{"left":0.9149306,"top":0.2488889,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Ivanov","depth":13,"bounds":{"left":0.9454861,"top":0.25111112,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":17,"bounds":{"left":0.87951386,"top":0.36277777,"width":0.07395833,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Aneliya Angelova to your main screen","depth":13,"bounds":{"left":0.7607639,"top":0.5088889,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else","depth":13,"bounds":{"left":0.7885417,"top":0.50666666,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Aneliya Angelova","depth":13,"bounds":{"left":0.8190972,"top":0.5088889,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-939566475183226600
|
-2879856849629225966
|
visual_change
|
hybrid
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
6
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Unpin Nikolay Yankov's presentation from your main screen
You can't unmute someone else's presentation
More options for Nikolay Yankov
Zoom in
Open in new window
Enter Full Screen
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
Pin Nikolay Ivanov to your main screen
You can't unmute someone else
More options for Nikolay Ivanov
Nikolay Ivanov
Pin Aneliya Angelova to your main screen
You can't unmute someone else
More options for Aneliya Angelova
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp> 0.§ [Platform] Refinemen... 45 m left100% C8• Mon 18 May 16:15:40meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.comNikolay Yankov (Presenting)+Mon 18 May 16:16(18® For you© Recent|# Starred8? Apps0 Spacesacarco(9 Service-DeskJiminny (Old)Jiminny (New)1 CD Platform TeamCID Capture Team|IID Enterprise Stabllity L.(ID Processing TeamIID SE Kanban|= More spaces= FiltersB Dashboards6 OperationsE3 Confluence38 Team-* Customise sidebarC Atte© OursCostaopxC AlOIPpe0 3m0 smlo Jim23 SSH• Home | SalesforceE Datadog*xISpacesJminny (New)C. Add parem#JY-20679Enable users to use their new activity types• DescriptionCurrently when customers create new activity types in their CRM, we ask them to create a new Playbook in order to fetch them. However now with the activity typeautomacons that we have - this means they need to copy ar or their prompes egan. wnich takes a nuge amourk or troeFor SF, Hubspot, Copper, Bullhorn, Close and Pipedrive (anything apart from Zoho)• when a new activity type is created in the CRM and fetched in Jiminny - add the activity type (in disabled state) to all the playbooks where activity type field ischeter• if a customer manualy creates an activity type in Jiminny - show them a warning message on the top saying "Tf you want this activity type to syng to your CRM oneach call - make sure to have ik in your CRM with the exact same name" - use this F https://www.figma.com/design/XcUety9mxSFiz8KosLAUn/Project-Phoenix?node-id=7483-2600188t-uJOZVbpzYIpC5PAy-1 [Connect your Fioma account• the message should stay only while they are adding the activity type - if they leave or refresh the page it should disappear. Use the info message that we haveSubtasksAdd subtaskLinked work itemsAdd linked work item~ ActivityWork logSuggest a reply.…..Who is working on this…..?Status update...Pro tip: press M to commenZ tY-* Prolinsights & CoachinC DevAsk RovoBackiogyIX Improve StoryDetailsAssignee8 UnassignedAssign to meKUpOrtE& Galya Dimitrova• Quick start developmentLink this work item to your code byincluding keys when creating abranch, commit, or pull request|below. Learn moreDismissQ Open with VS CodeCreate branch* Create commit)ComponentsPistonNotkstory point esomiatoNikolay YankovNikolay IvanovAneliya AngelovaGalya Dimitrova4:15 PM | [Platform] Refinement ®Lukas Kovalik1:02:08Ill...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
54559
|
1898
|
1
|
2026-05-18T13:15:44.219102+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110144219_m1.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
6
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
Pin Nikolay Ivanov to your main screen
You can't unmute someone else
More options for Nikolay Ivanov
Nikolay Ivanov
Pin Aneliya Angelova to your main screen
You can't unmute someone else
More options for Aneliya Angelova
Aneliya Angelova
Pin Galya Dimitrova to your main screen
Mute Galya Dimitrova's microphone
More options for Galya Dimitrova
Galya Dimitrova
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
Others might see more of your background. Click to view your full video.
4:15
PM
[Platform] Refinement 🔍
[Platform] Refinement 🔍
Audio settings
Turn on microphone
Video settings
Turn off camera...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - [Platform] Refinement 🔍","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.8411111,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.0,"top":0.8794444,"width":0.033680554,"height":0.03888889},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.0,"top":0.91833335,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.95666665,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.88680553,"top":0.08722222,"width":0.04097222,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":22,"bounds":{"left":0.9145833,"top":0.09888889,"width":0.0048611113,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Take notes with Gemini","depth":14,"bounds":{"left":0.93333334,"top":0.08722222,"width":0.025,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Take notes with Gemini","depth":17,"bounds":{"left":0.9361111,"top":0.09888889,"width":0.06388891,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Gemini","depth":17,"bounds":{"left":0.96666664,"top":0.09888889,"width":0.028125,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":17,"bounds":{"left":0.96458334,"top":0.08833333,"width":0.023611112,"height":0.037777778},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Pin Nikolay Yankov to your main screen","depth":13,"bounds":{"left":0.375,"top":0.29777777,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Mute Nikolay Yankov's microphone","depth":13,"bounds":{"left":0.4027778,"top":0.29555556,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Yankov","depth":13,"bounds":{"left":0.43333334,"top":0.29777777,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Nikolay Yankov","depth":17,"bounds":{"left":0.25555557,"top":0.45277777,"width":0.07673611,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Nikolay Ivanov to your main screen","depth":13,"bounds":{"left":0.76458335,"top":0.29777777,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else","depth":13,"bounds":{"left":0.79236114,"top":0.29555556,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Ivanov","depth":13,"bounds":{"left":0.8229167,"top":0.29777777,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":17,"bounds":{"left":0.64479166,"top":0.45277777,"width":0.07395833,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Aneliya Angelova to your main screen","depth":13,"bounds":{"left":0.2934028,"top":0.65555555,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else","depth":13,"bounds":{"left":0.32118055,"top":0.6533333,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Aneliya Angelova","depth":13,"bounds":{"left":0.3517361,"top":0.65555555,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Aneliya Angelova","depth":17,"bounds":{"left":0.21458334,"top":0.81,"width":0.088541664,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Galya Dimitrova to your main screen","depth":13,"bounds":{"left":0.60694444,"top":0.65555555,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Mute Galya Dimitrova's microphone","depth":13,"bounds":{"left":0.63472223,"top":0.6533333,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Galya Dimitrova","depth":13,"bounds":{"left":0.6652778,"top":0.65555555,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Galya Dimitrova","depth":17,"bounds":{"left":0.5375,"top":0.81,"width":0.08194444,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"You’re continuously framed","depth":13,"bounds":{"left":0.8038194,"top":0.71166664,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Backgrounds and effects","depth":13,"bounds":{"left":0.834375,"top":0.71166664,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Lukas Kovalik","depth":13,"bounds":{"left":0.86493057,"top":0.7138889,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Lukas Kovalik","depth":17,"bounds":{"left":0.7121528,"top":0.86833334,"width":0.06875,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Others might see more of your background. Click to view your full video.","depth":14,"bounds":{"left":0.96770835,"top":0.86444443,"width":0.019444445,"height":0.031111112},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4:15","depth":12,"bounds":{"left":0.050347224,"top":0.9444444,"width":0.021180555,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PM","depth":12,"bounds":{"left":0.075,"top":0.9444444,"width":0.016666668,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"[Platform] Refinement 🔍","depth":12,"bounds":{"left":0.10902778,"top":0.9111111,"width":0.12881945,"height":0.08888888},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"[Platform] Refinement 🔍","depth":15,"bounds":{"left":0.10902778,"top":0.9438889,"width":0.12881945,"height":0.023333333},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Audio settings","depth":13,"bounds":{"left":0.32118055,"top":0.9288889,"width":0.06111111,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn on microphone","depth":13,"bounds":{"left":0.34895834,"top":0.9288889,"width":0.033333335,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":true,"is_selected":false},{"role":"AXButton","text":"Video settings","depth":13,"bounds":{"left":0.38784721,"top":0.9288889,"width":0.06111111,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn off camera","depth":13,"bounds":{"left":0.415625,"top":0.9288889,"width":0.033333335,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-8654119809327140932
|
-2852511132424742800
|
visual_change
|
hybrid
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
6
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
Pin Nikolay Ivanov to your main screen
You can't unmute someone else
More options for Nikolay Ivanov
Nikolay Ivanov
Pin Aneliya Angelova to your main screen
You can't unmute someone else
More options for Aneliya Angelova
Aneliya Angelova
Pin Galya Dimitrova to your main screen
Mute Galya Dimitrova's microphone
More options for Galya Dimitrova
Galya Dimitrova
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
Others might see more of your background. Click to view your full video.
4:15
PM
[Platform] Refinement 🔍
[Platform] Refinement 🔍
Audio settings
Turn on microphone
Video settings
Turn off camera
FirefoxFileEdit ViewHistoryBookmarksProfiles→ToolsWindowHelp‹>0meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com# [Platform] Refinemen.. 45 m left100% <78• Mon 18 May 16:15:43Nikolay Yankov (Presenting)6Nikolay YankovNikolay IvanoyAneliya AngelovaGalya DimitrovaLukas Kovalik4:15 PM | [Platform] Refinement ®1:02:11Lộ3...
|
54558
|
NULL
|
NULL
|
NULL
|
|
54560
|
1898
|
2
|
2026-05-18T13:15:47.268509+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110147268_m1.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
5
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
Pin Nikolay Ivanov to your main screen
You can't unmute someone else
More options for Nikolay Ivanov
Nikolay Ivanov
Pin Aneliya Angelova to your main screen
You can't unmute someone else
More options for Aneliya Angelova
Aneliya Angelova
Pin Galya Dimitrova to your main screen
Mute Galya Dimitrova's microphone
More options for Galya Dimitrova
Galya Dimitrova
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
Others might see more of your background. Click to view your full video.
4:15
PM
[Platform] Refinement 🔍...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - [Platform] Refinement 🔍","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.8411111,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.0,"top":0.8794444,"width":0.033680554,"height":0.03888889},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.0,"top":0.91833335,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.95666665,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.88680553,"top":0.08722222,"width":0.04097222,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5","depth":22,"bounds":{"left":0.9145833,"top":0.09888889,"width":0.0048611113,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Take notes with Gemini","depth":14,"bounds":{"left":0.93333334,"top":0.08722222,"width":0.025,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Take notes with Gemini","depth":17,"bounds":{"left":0.9361111,"top":0.09888889,"width":0.06388891,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Gemini","depth":17,"bounds":{"left":0.96666664,"top":0.09888889,"width":0.028125,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":17,"bounds":{"left":0.96458334,"top":0.08833333,"width":0.023611112,"height":0.037777778},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Pin Nikolay Yankov to your main screen","depth":13,"bounds":{"left":0.26145834,"top":0.3122222,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Mute Nikolay Yankov's microphone","depth":13,"bounds":{"left":0.2892361,"top":0.31,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Yankov","depth":13,"bounds":{"left":0.31979167,"top":0.3122222,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Nikolay Yankov","depth":17,"bounds":{"left":0.10729167,"top":0.48277777,"width":0.07673611,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Nikolay Ivanov to your main screen","depth":13,"bounds":{"left":0.68645835,"top":0.3122222,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else","depth":13,"bounds":{"left":0.71423614,"top":0.31,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Ivanov","depth":13,"bounds":{"left":0.7447917,"top":0.3122222,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":17,"bounds":{"left":0.53229165,"top":0.48277777,"width":0.07395833,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Aneliya Angelova to your main screen","depth":13,"bounds":{"left":0.15729167,"top":0.70111114,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"You can't unmute someone else","depth":13,"bounds":{"left":0.18506944,"top":0.6988889,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Aneliya Angelova","depth":13,"bounds":{"left":0.215625,"top":0.70111114,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Aneliya Angelova","depth":17,"bounds":{"left":0.057291668,"top":0.87166667,"width":0.088541664,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Galya Dimitrova to your main screen","depth":13,"bounds":{"left":0.47430557,"top":0.70111114,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Mute Galya Dimitrova's microphone","depth":13,"bounds":{"left":0.50208336,"top":0.6988889,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Galya Dimitrova","depth":13,"bounds":{"left":0.5326389,"top":0.70111114,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Galya Dimitrova","depth":17,"bounds":{"left":0.37395832,"top":0.87166667,"width":0.08194444,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"You’re continuously framed","depth":13,"bounds":{"left":0.7899306,"top":0.6988889,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Backgrounds and effects","depth":13,"bounds":{"left":0.8204861,"top":0.6988889,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Lukas Kovalik","depth":13,"bounds":{"left":0.8510417,"top":0.70111114,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Lukas Kovalik","depth":17,"bounds":{"left":0.69131947,"top":0.87166667,"width":0.06875,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Others might see more of your background. Click to view your full video.","depth":14,"bounds":{"left":0.9607639,"top":0.86722225,"width":0.019444445,"height":0.031111112},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4:15","depth":12,"bounds":{"left":0.050347224,"top":0.9444444,"width":0.021180555,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PM","depth":12,"bounds":{"left":0.075,"top":0.9444444,"width":0.016666668,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"[Platform] Refinement 🔍","depth":12,"bounds":{"left":0.10902778,"top":0.9111111,"width":0.12881945,"height":0.08888888},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"}]...
|
-5901362812685594277
|
-2870594525028820974
|
visual_change
|
hybrid
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
People
5
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
Pin Nikolay Ivanov to your main screen
You can't unmute someone else
More options for Nikolay Ivanov
Nikolay Ivanov
Pin Aneliya Angelova to your main screen
You can't unmute someone else
More options for Aneliya Angelova
Aneliya Angelova
Pin Galya Dimitrova to your main screen
Mute Galya Dimitrova's microphone
More options for Galya Dimitrova
Galya Dimitrova
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
Others might see more of your background. Click to view your full video.
4:15
PM
[Platform] Refinement 🔍
FirefoxFileEdit→ViewHistoryBookmarksProfilesToolsWindowHelp<→0meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com§ [Platform] Refinemen... 45 m left100%8• Mon 18 May 16:15:46a0 5+Nikolay YankovNikolay Ivanov4Aneliya AngelovaGalya DimitrovaLukas Kovalik4:15 PM | [Platform] Refinement ®1:02:14...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
54561
|
1898
|
3
|
2026-05-18T13:15:50.237363+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110150237_m1.jpg...
|
Firefox
|
Meet - [Platform] Refinement 🔍 — Work
|
1
|
meet.google.com/cxs-eips-npt?authuser=lukas.kovali meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Galya Dimitrova (Presenting, annotating)
Galya Dimitrova (Presenting, annotating)...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - [Platform] Refinement 🔍","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.8411111,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.0,"top":0.8794444,"width":0.033680554,"height":0.03888889},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.0,"top":0.91833335,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.95666665,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXHeading","text":"Galya Dimitrova (Presenting, annotating)","depth":12,"bounds":{"left":0.07534722,"top":0.101111114,"width":0.18263888,"height":0.022222223},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Galya Dimitrova (Presenting, annotating)","depth":13,"bounds":{"left":0.07534722,"top":0.10222222,"width":0.18263888,"height":0.020555556},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
1402193066757241454
|
1008891204569716423
|
visual_change
|
accessibility
|
NULL
|
Meet - [Platform] Refinement 🔍
Close tab
New Tab
O Meet - [Platform] Refinement 🔍
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Galya Dimitrova (Presenting, annotating)
Galya Dimitrova (Presenting, annotating)...
|
54560
|
NULL
|
NULL
|
NULL
|
|
54562
|
1898
|
4
|
2026-05-18T13:15:51.749974+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110151749_m1.jpg...
|
PhpStorm
|
faVsco.js – Crm/…/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEdit ViewHistoryBookmarksProfiles→Tools FirefoxFileEdit ViewHistoryBookmarksProfiles→ToolsWindowHelp<→0meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com§ [Platform] Refinemen... 45 m left100% <8• Mon 18 May 16:15:51=Galya Dimitrova (Presenting, annotating)6Galyà DimitrovaNikolay YankovAneliya AngelovaNikolay IvanovLukas Kovalik4:15 PM | [Platform] Refinement ®1:02:18Lộ3...
|
NULL
|
5832555918897870215
|
NULL
|
click
|
ocr
|
NULL
|
FirefoxFileEdit ViewHistoryBookmarksProfiles→Tools FirefoxFileEdit ViewHistoryBookmarksProfiles→ToolsWindowHelp<→0meet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.com§ [Platform] Refinemen... 45 m left100% <8• Mon 18 May 16:15:51=Galya Dimitrova (Presenting, annotating)6Galyà DimitrovaNikolay YankovAneliya AngelovaNikolay IvanovLukas Kovalik4:15 PM | [Platform] Refinement ®1:02:18Lộ3...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
54563
|
1899
|
0
|
2026-05-18T13:15:52.006684+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110152006_m2.jpg...
|
PhpStorm
|
faVsco.js – Crm/…/Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavicarecodeLaravelKeractorrTavsco.s? PhostormVIewINavicarecodeLaravelKeractorrTavsco.s?9 master kProiectRinaCentralVideoSalesforceIm Salesloft> D TalkdeskDTeams> D TelusD Twilio>@ TwilioFlex_ I willorlexDireet• _ I willoVideo_Uploader› _ Vonagewxant>400mZoomBot> ZoomPhoneC) ActivitvcrmFieldsResolver.ohpC) ActivitvLoaService.oho© ActivitvProviderClient.ohn(C) ActivitvProviderRedistrv.oho© ActivityProviderService.php© CallDenormalizerRegistry.phpC) [EMAIL]© DatalmportHandlerInterface.php© MeetingBotService.php© ParticipantConsentService.php(c) DarticinanteService nhnT PecnonceValidation Trait nhnT SalesforceGetUserTrait.php- S/DenormaliserMainCrmDatalrait.onp@ TrackRecordinoFllesizeservice.one©1rаскkecoraingsizetntorcer.onpT ValidateEmitProspectEventTrait.phpC AjReports0 AvatarMColondar0 Conference0 Crm> 0 Bullhornv closeOnoortunitvsvncstrateav• ProcessonProspectSearchStrateav• M TranslatorC) Client.oho() CloseSxceotion.ohoC) FieldDefinitions.onvc) SieldValueConverter ohn•) Service nhnC) StandardFioldMetadata nhn> MConnen© SoftPhoneManager.phpC) CoreUserRequest.pnpscimProvistoning.ong© CoreUser.php© ACtivity/.../Service.phg© Crm/../Service.php xclass Service extends BaseService 1mpLementsm A8 A39 M5 лV127 6t>132 0141 0)169 ©184 0189 Gt>194 015213 €pubLic tunccion normalizevalue scrino stlelalyoe, scrino stielovalue, oool sincernal = tause. Scprotected function getrieldivoeso: arrav....protected function getfields(string Scrmobnect): arrav.....* dinheritdocpublic function getDefaultFields(string $activityType): arrayt...* Ginhenitdocpublic function getDefaultActivityField(string $activityType): Field// Set up the activity field as the default Type./** @var Field $activityField */SactivityField = $this->config->fields->where([l"crn_provlder-ld → "type,'object_type' => $activityType,])->firstO:return sactivityrield* dinherztdocpublic function aetSupportedPlavbookTvnes@= arravS…..;public function getDealInsiahtsFields@: arrav{...}orotected function getdefaultFollowuoLavoutFields(strina Sactivitvivoe): arrav-i...apublic function syncFields(): voidSthis->cvncStandardFieldsoCthis->cvncCustomFields0•* @important Works only for custom fields= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]& console [EU]A console [STAGING]© CoachingFeedbackCoachUserln.php Xstohedeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 Ф >34 đ >45 đ >~ 118—119'136 đt ›_ 146 Gt ›-150151 G>1usadeorivate const int No GROUP 10 = 9993 usagesorivate UserRenository suserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection...hpublic function toArray: array{...hprivate function getOptions(): array{...}public function getValue®: array{...}private function getDefaultValue: array{...}public function aetValidationRules(2strina Sorefix = null): arrav{...}lpublic function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}40m let• Mon 18 May 10.10.0cascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSClM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for method9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ATTR ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabled•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/Ucer/M PoleAttr.nho 417ites/User/ ẞ RoleAttrTest.nhn +224* Reiect alliiAccent allAsk anvthina (&4-L)« Code SWF-1.6W Windsurf Teams 171-26UTE.fo 4 spaces...
|
NULL
|
8686833609892986851
|
NULL
|
click
|
ocr
|
NULL
|
PhostormVIewINavicarecodeLaravelKeractorrTavsco.s? PhostormVIewINavicarecodeLaravelKeractorrTavsco.s?9 master kProiectRinaCentralVideoSalesforceIm Salesloft> D TalkdeskDTeams> D TelusD Twilio>@ TwilioFlex_ I willorlexDireet• _ I willoVideo_Uploader› _ Vonagewxant>400mZoomBot> ZoomPhoneC) ActivitvcrmFieldsResolver.ohpC) ActivitvLoaService.oho© ActivitvProviderClient.ohn(C) ActivitvProviderRedistrv.oho© ActivityProviderService.php© CallDenormalizerRegistry.phpC) [EMAIL]© DatalmportHandlerInterface.php© MeetingBotService.php© ParticipantConsentService.php(c) DarticinanteService nhnT PecnonceValidation Trait nhnT SalesforceGetUserTrait.php- S/DenormaliserMainCrmDatalrait.onp@ TrackRecordinoFllesizeservice.one©1rаскkecoraingsizetntorcer.onpT ValidateEmitProspectEventTrait.phpC AjReports0 AvatarMColondar0 Conference0 Crm> 0 Bullhornv closeOnoortunitvsvncstrateav• ProcessonProspectSearchStrateav• M TranslatorC) Client.oho() CloseSxceotion.ohoC) FieldDefinitions.onvc) SieldValueConverter ohn•) Service nhnC) StandardFioldMetadata nhn> MConnen© SoftPhoneManager.phpC) CoreUserRequest.pnpscimProvistoning.ong© CoreUser.php© ACtivity/.../Service.phg© Crm/../Service.php xclass Service extends BaseService 1mpLementsm A8 A39 M5 лV127 6t>132 0141 0)169 ©184 0189 Gt>194 015213 €pubLic tunccion normalizevalue scrino stlelalyoe, scrino stielovalue, oool sincernal = tause. Scprotected function getrieldivoeso: arrav....protected function getfields(string Scrmobnect): arrav.....* dinheritdocpublic function getDefaultFields(string $activityType): arrayt...* Ginhenitdocpublic function getDefaultActivityField(string $activityType): Field// Set up the activity field as the default Type./** @var Field $activityField */SactivityField = $this->config->fields->where([l"crn_provlder-ld → "type,'object_type' => $activityType,])->firstO:return sactivityrield* dinherztdocpublic function aetSupportedPlavbookTvnes@= arravS…..;public function getDealInsiahtsFields@: arrav{...}orotected function getdefaultFollowuoLavoutFields(strina Sactivitvivoe): arrav-i...apublic function syncFields(): voidSthis->cvncStandardFieldsoCthis->cvncCustomFields0•* @important Works only for custom fields= custom.log= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console [PROD]& console [EU]A console [STAGING]© CoachingFeedbackCoachUserln.php Xstohedeclare(scrict_cypes=l)nnamespace Jiminny Component Activ1tySearch rilterbetin1t1on*› use ...final class CoachingFeedbackCoachUserIn extends FilterDefinition implements ValidatedFilterDefinitionInterface29 Ф >34 đ >45 đ >~ 118—119'136 đt ›_ 146 Gt ›-150151 G>1usadeorivate const int No GROUP 10 = 9993 usagesorivate UserRenository suserRenository:public function __construct(UserRepository $userRepository)f...hpublic function shouldApplyQueries: boolf...}public function getQueries: FilterDefinitionQueryCollection...hpublic function toArray: array{...hprivate function getOptions(): array{...}public function getValue®: array{...}private function getDefaultValue: array{...}public function aetValidationRules(2strina Sorefix = null): arrav{...}lpublic function aetSortOrder@: intf...}public function shouldBeIncluded(Team $team): bool{...}40m let• Mon 18 May 10.10.0cascadeImplement Trial OwneFixing Redis Rate LimSalestorce Token FalliSClM Role Manageme+0 ..inp on LineWARN Metadata found iin doc-comment for method9/ 10 tacke done• Write unit tests for SCIM role management10) Contact Mario to vodate customer10 / 10 tasks doneselm role managementtor leamtallor has been successtully implemented. Here's what was completed:Implementation Summary:1. CoreUserRequest DTO - Added role parsina from SCIM requests2. SCIM Constants - Added META ROLES and USER_PATCH_ATTR ROLES constants3. RoleAttr Mutator - Created mutator for handlina role assianments via SCIM PATCH operations4. ScimProvisioning - Updated to handle roles during user creation with proper validation5. ListenerRoleCannotHaveAdminOrManagerPermissionRule - Created validation rule to prevent listener role fromhaving admin/manager permissions6. CoreUser Response - Updated to include roles in SClM responses7. Unit Tests - Created comprehensive tests for the validation ruleKey Features:• Customers can now assign roles via SCIM (recorder, recorder & voice, analyst, listener, admin, manager)• Validation ensures listener role is onlv assianable if the LISTENER ROLE feature flaa is enabled•LIstener role cannot de comolned with aamin or manager permissions• Uses existing UpdateUserRolesAction for role syncing and logging• Follows platform validation rules (DependentRolesRule, ListenerRoleRequiresListenerFeatureRule)Next Step: Please contact Mario to update the customer that SCIM role management is now available for Teamtailorintearationposwtn chaten vapp/Component/SClM/ Constants.php +3app/Component/SCIM/ @ ScimProvisioning.php +85-15nse/ CoreUser.php +21ann/Comnonent/SCIM/Mutatore/Attributes/Ucer/M PoleAttr.nho 417ites/User/ ẞ RoleAttrTest.nhn +224* Reiect alliiAccent allAsk anvthina (&4-L)« Code SWF-1.6W Windsurf Teams 171-26UTE.fo 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
54564
|
1898
|
5
|
2026-05-18T13:15:53.269401+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110153269_m1.jpg...
|
PhpStorm
|
faVsco.js – Crm/…/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>114 incoming commits<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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}]...
|
-6555592100605644990
|
-8204429277455601722
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
+FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp: [Platform) Refinemen…45 m leftmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.comGalya Dimitrova (Presenting, annotating)ChromeFileEditHistoryBookmarksProfiesTabWindowHelp$Mon 18 May 14:15Wellt Pro+ PE• Dis• ProTheCovHoO AIEO AutraAulEl Recapp.jminny.com/settings/organizatlon/ai-automation/crm-filling& WorkRelaunch to updateClaude (MCP)|* Claude (MCP)• Jiminny ProdJiminny EU ProdJiminny MercuryJiminny StagingJiminny Saturn© Jiminny QAIJiminny QAUserpilot• Salesforce3 OutlookOrganization SettingsAl Automation• CRM FilingAdd AutomationGeneralCRM FillingCall Scoring |UsersJAXCHKuCTTLANSTeamsCUSTOMER SUCCESS© DeleteIntegrations> • Customer Success487 fimes for hast. 2 digsJob TitlesAUTOMATION NAVECustomer Success> • Sales - Forecast36 times for last 7 davsActivity> • Sales - Manual filling6 times tor last 7 davsОВЛЕСТDealRecording$> • Petko ClosedWon Triggers |4 times for last 7 daysAl Context• TestO times for last 7 desClient Success xCustomer Success BG x)Al AutomationPlanhat Data Enrichment; Sales& AUTOMATION DISABLEDSidekickDeal InsightsWNNCA UT(New Conference *New Email xNew Dialler call xDeal Stage Changed xVocabularyTopicsCommitted xDowngrade RiskClosed Won xClosed Lost xChurn Risk xLikely to Renew xKey Words ScoringPlaybooks & Coaching Frameworks• Auto syncManual syncNotificationsCancel|Settings8• Mon 18 May 16:15:52ya DimitrovaNikolay YankovAneliya AngelovaNikolay Ivanovmeet.google.com is sharing your screon.Stop sharingHidel4:15 PM| [Platform] RefinementLukas Kovalik1:02:20Lộ3...
|
54562
|
NULL
|
NULL
|
NULL
|
|
54565
|
1898
|
6
|
2026-05-18T13:15:56.321814+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-18/1779 /Users/lukas/.screenpipe/data/data/2026-05-18/1779110156321_m1.jpg...
|
PhpStorm
|
faVsco.js – Crm/…/Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
+FirefoxFileProfiles:[Platform) Refinemen….45 m le +FirefoxFileProfiles:[Platform) Refinemen….45 m left• Mon 18 May 16:15:56|EditViewHistoryBookmarksToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.comGalya Dimitrova (Presenting, annotating)ChromeFileEditHistoryBookmarksProfilesTabWindowHelp8• Mon 18 May 14:15t Prot PE© Dis• Pror | Ta The• CouО: HonO AIEO: Aut©, traAut8 Recapp.jiminny.com/settings/organization/ai-automation/crm-filling3 WorkRelaunch to updateClaude (MCP)|t Claude (MCP)• Jiminny ProdJiminny Mercury Jiminny StagingJiminny Saturn• Jiminny QAI|Jiminny QAw Userpilot|• Salesforce9 OutlookOrganization SettingsAl Automation• CRM Filling÷ ExportAdd AutomationGeneralCRM FillingCall Scoring |UsersЗАХCHIXUECTTLAMATeamsCUSTOMER SUCCESS© DeleteIntegrationsJob Titles> • Customer Success› ©Sales - Forecast487 times for last 7 daysAUTOMATION NAMECustomer SuccessAdd heldActivity> • Sales - Manual filling6 times for last 7 daysОВJЕСТDeal$Recording> • Petko ClosedWon Triggers |4 times for last 7 daysAl Context• TestO times for last 7 daysTEAMClient Success x)Customer Success BG *)Al Automation› Planhat Data Enrichment: Sales& AUTOMATION DISABLEDSidekickDeal InsightsWRNCR UTNew Conference x(New Dialler call *]Deal Stage ChangedNew Email xVocabularyTopicsCommitted xDowngrade Risk xClosed Won xClosed Lost xChurn Risk xLikely to Renew xKey Words ScoringPlaybooks & Coaching Frameworks• Auto syncManual syncNotificationsCancelSettingsmeet.google.com is sharing your screen.Stop sharingHide200mGálya DimitrovaAneliya AngelovaNikolay YankovNikolay Ivanov4:15 PM| [Platform] RefinementLukas Kovalik1:02:23Lộ3...
|
NULL
|
8506828165517590091
|
NULL
|
visual_change
|
ocr
|
NULL
|
+FirefoxFileProfiles:[Platform) Refinemen….45 m le +FirefoxFileProfiles:[Platform) Refinemen….45 m left• Mon 18 May 16:15:56|EditViewHistoryBookmarksToolsWindowHelpmeet.google.com/cxs-eips-npt?authuser=lukas.kovalik%40jiminny.comGalya Dimitrova (Presenting, annotating)ChromeFileEditHistoryBookmarksProfilesTabWindowHelp8• Mon 18 May 14:15t Prot PE© Dis• Pror | Ta The• CouО: HonO AIEO: Aut©, traAut8 Recapp.jiminny.com/settings/organization/ai-automation/crm-filling3 WorkRelaunch to updateClaude (MCP)|t Claude (MCP)• Jiminny ProdJiminny Mercury Jiminny StagingJiminny Saturn• Jiminny QAI|Jiminny QAw Userpilot|• Salesforce9 OutlookOrganization SettingsAl Automation• CRM Filling÷ ExportAdd AutomationGeneralCRM FillingCall Scoring |UsersЗАХCHIXUECTTLAMATeamsCUSTOMER SUCCESS© DeleteIntegrationsJob Titles> • Customer Success› ©Sales - Forecast487 times for last 7 daysAUTOMATION NAMECustomer SuccessAdd heldActivity> • Sales - Manual filling6 times for last 7 daysОВJЕСТDeal$Recording> • Petko ClosedWon Triggers |4 times for last 7 daysAl Context• TestO times for last 7 daysTEAMClient Success x)Customer Success BG *)Al Automation› Planhat Data Enrichment: Sales& AUTOMATION DISABLEDSidekickDeal InsightsWRNCR UTNew Conference x(New Dialler call *]Deal Stage ChangedNew Email xVocabularyTopicsCommitted xDowngrade Risk xClosed Won xClosed Lost xChurn Risk xLikely to Renew xKey Words ScoringPlaybooks & Coaching Frameworks• Auto syncManual syncNotificationsCancelSettingsmeet.google.com is sharing your screen.Stop sharingHide200mGálya DimitrovaAneliya AngelovaNikolay YankovNikolay Ivanov4:15 PM| [Platform] RefinementLukas Kovalik1:02:23Lộ3...
|
NULL
|
NULL
|
NULL
|
NULL
|