|
85765
|
2941
|
6
|
2026-05-28T13:08:33.216594+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973713216_m2.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rapstomViewCoocWindowFV faVsco.s ~#12121 on JY-209 rapstomViewCoocWindowFV faVsco.s ~#12121 on JY-20963-fx-inwnapers.oh©ActivityControlier.php=custom.loglaravel.logA SF fiminny@localhost)HS Jocal (jiminny(blocalhost)& console (PROD) Xuewholeooserver.ono Tesmuonsc) eienupnpA console [STAGING>E HubspolE InicaradonAdeTX Autow>E Listener:INTERVAL 38 DAY.class ServiceTest extends TestCasebY 0.20u.caszlBY sns count DESCu.name u.sotcphone nunbenv saestorch> D FieldsOpoorunityMatcherOpportunitvSvncStrate26372638s-Saccentlor.matscesu.r-17-783E782* fron ceans where 20 =1Sthis->assertLessThan0rEqual( maximum: 25, strlen(Sresult)):2648• ProspectsearchStrategServicet raits©elientrest.php© DecorateActivityTest.p 2043po ated, oestn tteshublic function testParseContactMob{leWlithCountryCode: voicSservaee - sunasosgerseracenocklshScreData = "MobilePhone' => *0791112345691#NCATU.nd, CASE WHEN UINdETOUNerd THEN ouner" EsE END AS userd© FieldDetinitionsTest.phCCeTcwmaet ame#i PavtoadRuilderTest.ohehut nvokernVethttnodmethodName: "parseContactMobile', Sservice. ('GB", Scrabatallormen fo FRil soctal accnuntssa"Quer Rt dertest nhnQuervHandierTast ooduSoassenorOrKorURsers u on u.1d = sa.soclable_1cPargTncaontd n.teandOuerviteratorTast mhouseassenornnestor swth acheteessukeu.tean_1d = 1117 and sa.provider = "hubspot":*EROM actMtes WHEOF Wund to binl18a2tetth-2147-1817.0144-40480695926801= Musd: " 70033459 VES© QueryResultsTests.phpc Semcetescons© SyncBatchRedisServickRacASanicatast obo*EROM actM tes WHEOF mund to binl:255208/3-800/-1781-0276.66648288185c01= Musde 80186192 M0public functiion testinport@pportunitySkiosilhenProfileNotFoundO: voidf...;4 lof2odtd*EROM eon confaurazons WHERF 3d = 1953%* FROM teans WHERE id = 1117:+Eoon ucene whene 3d= 30240.Changes 5 filesE.env.local apc© JiminnyDebugCommand.php app/Console/Commands1L P +→ Side-by-side viewer -8 41e8c6bS tests/Unit/Services/Cpe loagina.ono contia@ PlaybackService.pho app/ServicesSresult & sereinvokeenivatekethodd parsetontactPhone'. Sseruice, Scountnydode. Seonintatbl• Salesforceservicelest.phpyUnversioned Filce 9 ficdE.enw.nikilocal andclass Servicelest extends Testlase: |Tests|Unit|Services|Crn|SailesforcelServiceTestpublic static fueenw.other aog@ GanAccossA ReporteTest.oho teste/Unit/PoliciosSource: /Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce/ServiceTest.phpreturnCreateWock.skiminny?eootcecteommandlohoang/consa.elcommandsln=> 'MobilePhone' a>tu favicon.ico public=iktyandR.raw.sal.querv.sol.aoolEconuind e CihtersnotComudAoMAAWENdS mulateWebhookscommand.oho.ano/.Concole/Commandis/crm/HubsootM. WERHOOK FILTFRING IMDIFMENTATION md anrSresult = self::invokePrivateMethod("parseContactMobile', Sservice. [ScountryCode, ScrnDatal)Sthis-›assertSame(Sexpected, Sresult)ouobzc tuncczon cesc.noor.uppor.unzcyskzoswhenrrorztenocround: vozd# console fiaum045 A1 A41 У 66 4TO0У L7ind co mey to.uorssServiceTesteeendhles orchwerwicee+0.destsrunxes drvkees/arm/S bestore Servtcer/st.php -f itervetest/brsecontact' 2562PHPUnit 11.5.55 by Sebastian Bergmann and contributors.contelouratciontnore/tinnyl ohounx"11/ 11 (100%Тдоe: 00:10.908, Меnогy: 60.00 мбAl 11 new and existing testParseContact* tests pass. The deprecations are pre-existing PHPUnit warningsLet ma aiso ar'cklv nin tha ull Saruf caTast to contim nothida is brokaa.vendor/bin/phpun:PHPUnzt 11.5.55 by Sebastzan Beramann and contributors.RuntrOHp SH1RlA ARoNecantalll+ < Code$AdaptiveotterencePurtent vereionSresult = self::invokePrivateMethod"parseContactPhone', Sservice, [ScountryCode, Scrmbata)):SthisobassentSane Sexoected, Sresulaspublic function testParseContactPhoneWithValidNumberO: voidSservice = Sthis->getServiceMockO:ScrmData = "Phone' a>*[PHONE]')#SparsedNumber, Sext) = self::invokePrivatelethod("parseContactPhone', Sservice, null, ScraData):Sthis->assertArrayHasKey("phone'. SparsedNumber):Scnis->assertsane [PHONE] S08rsechunberpnone ohSthis->assertNull(Sext):oublie funetzion testPanseContactPhonellithExtensiionde voiaRemoe hise>odSeWCOCkOSoreData = f'Phone* => *[PHONE] cxt. 42'1.fSoansedMunber. Soxtl = self.sinvokePoivatckethod("nansefontactPhone" Sservice. F'US'. Sccelatal):W Windsurf Teams 2643:6 UTF-8 4 space-...
|
NULL
|
3848793598643661916
|
NULL
|
visual_change
|
ocr
|
NULL
|
rapstomViewCoocWindowFV faVsco.s ~#12121 on JY-209 rapstomViewCoocWindowFV faVsco.s ~#12121 on JY-20963-fx-inwnapers.oh©ActivityControlier.php=custom.loglaravel.logA SF fiminny@localhost)HS Jocal (jiminny(blocalhost)& console (PROD) Xuewholeooserver.ono Tesmuonsc) eienupnpA console [STAGING>E HubspolE InicaradonAdeTX Autow>E Listener:INTERVAL 38 DAY.class ServiceTest extends TestCasebY 0.20u.caszlBY sns count DESCu.name u.sotcphone nunbenv saestorch> D FieldsOpoorunityMatcherOpportunitvSvncStrate26372638s-Saccentlor.matscesu.r-17-783E782* fron ceans where 20 =1Sthis->assertLessThan0rEqual( maximum: 25, strlen(Sresult)):2648• ProspectsearchStrategServicet raits©elientrest.php© DecorateActivityTest.p 2043po ated, oestn tteshublic function testParseContactMob{leWlithCountryCode: voicSservaee - sunasosgerseracenocklshScreData = "MobilePhone' => *0791112345691#NCATU.nd, CASE WHEN UINdETOUNerd THEN ouner" EsE END AS userd© FieldDetinitionsTest.phCCeTcwmaet ame#i PavtoadRuilderTest.ohehut nvokernVethttnodmethodName: "parseContactMobile', Sservice. ('GB", Scrabatallormen fo FRil soctal accnuntssa"Quer Rt dertest nhnQuervHandierTast ooduSoassenorOrKorURsers u on u.1d = sa.soclable_1cPargTncaontd n.teandOuerviteratorTast mhouseassenornnestor swth acheteessukeu.tean_1d = 1117 and sa.provider = "hubspot":*EROM actMtes WHEOF Wund to binl18a2tetth-2147-1817.0144-40480695926801= Musd: " 70033459 VES© QueryResultsTests.phpc Semcetescons© SyncBatchRedisServickRacASanicatast obo*EROM actM tes WHEOF mund to binl:255208/3-800/-1781-0276.66648288185c01= Musde 80186192 M0public functiion testinport@pportunitySkiosilhenProfileNotFoundO: voidf...;4 lof2odtd*EROM eon confaurazons WHERF 3d = 1953%* FROM teans WHERE id = 1117:+Eoon ucene whene 3d= 30240.Changes 5 filesE.env.local apc© JiminnyDebugCommand.php app/Console/Commands1L P +→ Side-by-side viewer -8 41e8c6bS tests/Unit/Services/Cpe loagina.ono contia@ PlaybackService.pho app/ServicesSresult & sereinvokeenivatekethodd parsetontactPhone'. Sseruice, Scountnydode. Seonintatbl• Salesforceservicelest.phpyUnversioned Filce 9 ficdE.enw.nikilocal andclass Servicelest extends Testlase: |Tests|Unit|Services|Crn|SailesforcelServiceTestpublic static fueenw.other aog@ GanAccossA ReporteTest.oho teste/Unit/PoliciosSource: /Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce/ServiceTest.phpreturnCreateWock.skiminny?eootcecteommandlohoang/consa.elcommandsln=> 'MobilePhone' a>tu favicon.ico public=iktyandR.raw.sal.querv.sol.aoolEconuind e CihtersnotComudAoMAAWENdS mulateWebhookscommand.oho.ano/.Concole/Commandis/crm/HubsootM. WERHOOK FILTFRING IMDIFMENTATION md anrSresult = self::invokePrivateMethod("parseContactMobile', Sservice. [ScountryCode, ScrnDatal)Sthis-›assertSame(Sexpected, Sresult)ouobzc tuncczon cesc.noor.uppor.unzcyskzoswhenrrorztenocround: vozd# console fiaum045 A1 A41 У 66 4TO0У L7ind co mey to.uorssServiceTesteeendhles orchwerwicee+0.destsrunxes drvkees/arm/S bestore Servtcer/st.php -f itervetest/brsecontact' 2562PHPUnit 11.5.55 by Sebastian Bergmann and contributors.contelouratciontnore/tinnyl ohounx"11/ 11 (100%Тдоe: 00:10.908, Меnогy: 60.00 мбAl 11 new and existing testParseContact* tests pass. The deprecations are pre-existing PHPUnit warningsLet ma aiso ar'cklv nin tha ull Saruf caTast to contim nothida is brokaa.vendor/bin/phpun:PHPUnzt 11.5.55 by Sebastzan Beramann and contributors.RuntrOHp SH1RlA ARoNecantalll+ < Code$AdaptiveotterencePurtent vereionSresult = self::invokePrivateMethod"parseContactPhone', Sservice, [ScountryCode, Scrmbata)):SthisobassentSane Sexoected, Sresulaspublic function testParseContactPhoneWithValidNumberO: voidSservice = Sthis->getServiceMockO:ScrmData = "Phone' a>*[PHONE]')#SparsedNumber, Sext) = self::invokePrivatelethod("parseContactPhone', Sservice, null, ScraData):Sthis->assertArrayHasKey("phone'. SparsedNumber):Scnis->assertsane [PHONE] S08rsechunberpnone ohSthis->assertNull(Sext):oublie funetzion testPanseContactPhonellithExtensiionde voiaRemoe hise>odSeWCOCkOSoreData = f'Phone* => *[PHONE] cxt. 42'1.fSoansedMunber. Soxtl = self.sinvokePoivatckethod("nansefontactPhone" Sservice. F'US'. Sccelatal):W Windsurf Teams 2643:6 UTF-8 4 space-...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85764
|
2941
|
5
|
2026-05-28T13:08:23.829242+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973703829_m2.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormvewNeweNNCCoocWindowFV faVsco.|s ~#12121 o PhpStormvewNeweNNCCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-inwnapers.oh©ActivityControlier.phpuewholeooserver.ono Tesmuonsc) eienupnp>E HubspolE IntcaratonAoe>E Listener:class ServiceTest extends TestCasev saestorch› E FieldsOpoorunityMatcherOpportunitvSvncStrate26372638s-Saccentlor.matscesu.rSthis->assertLessThan0rEqual( maximum: 25, strlen(Sresult)):2648› ProspectsearchStrategServicetraits©elientrest.php© DecorateActivityTest.p 2643po ated, oestn tteshublic function testParseContactMob{leWlithCountryCode: voicSservaee - sunasosgerseracenocklshScreData = "MobilePhone' => *0791112345691#© FieldDetinitionsTest.phCCeTcwmaet ame#i PavtoadRuilderTest.ohehut nvokernVethttnod"parseContactMobile', Sservice. ('GB", Scrabatal"Quer Rt dertest nhnQuervHandierTast ooduSoassenorOrKorUROuerviteratorTast mhousorassenorn hostar s thl ecuetewssune© QueryResultsTests.php© ServiceTest.php© SyncBatchRedisServickRacASanicatast obopublic functiion testinport@pportunitySkiosilhenProfileNotFoundO: voidi...:4 lof2odtdXReM F OXG• Changes 5 files, updating*^L+→f Side-by-side viewer -841e8c6b5 tests/Unit/ServiceDo not ignore"© JiminnyDebugCommand.php app/Console/CommandsCnocnllt o ColtinuAhoon ustolothadld nancorantadtohano"pe loagina.ono contia@ PlaybackService.pho app/ServicesSthis-›assertSame(Sexpected, Sresulte servicetestonoyUnversioned filce 9 flas, uodatina.E.enw.nikilocal.aodpublic static function parseContactMobileProvider@: arrayeenw.other aog@ GanAccossA ReporteTest.oho teste/Unit/Policiosreturn ienocy hoozlernone recurns null'=> (['MobilePhone' => **1. null, nulz]createWlock.skiminny?eoot?ecteommandlonoaog/conso.e/commandsintu favicon.ico publicsldk.yandR.raw.sal.querv.sol.aoolaserynoae tniooesernechockioS mulateWebhookscommand.oho.ano/Concole/Commandis/Crm/Hubsoottresulsser.nvoke?owrche.hooldparertontcicrhooktrsenutce.countnweooe.scomihcl1sSthisonssentSane(Scxoccted, Sresult):M. WERHOOK FILTFRING IMDIFMENTATION md anrnunide tuncciontesairhor tonortunsysk nswhenProror ellor Soundioie wose=custom.logaraveuesA SF fiminny@localhost)HS Jocal (jminny@localhost)A console [STAGING17781TX AutowINTERVAL 38 DAYXbY 0.20u.caszlBY sns count DESCu.name,u.sorcphonenuaben* fron ceans where 20 =1& console (PROD) X# console fiaum045 A1 A41 У 66 4NCATCu,id. CASE WHEN u.id = thouner_id THEN • (oaner)* ELSE ** END) AS user_idormen fo FRil soctal accnuntssasers u on u.1d = sa.soclable_1cParGTnCoontd n.teandu.tean_1d = 1117 and sa.provider = "hubspot":*EROM actMtes WHEOF Wund to binl18a2tetth-2147-1817.0144-40480695926801= Musd: " 70033459 VES*EROM actM tes WHEOF mund to binl:255208/3-800/-1781-0276.66648288185c01= Musde 80186192 M0*EROM eon confaurazons MHEPF 3d = 1953%* FROM teans WHERE id = 1117:+Eoon licene whene a= 30240.X1€?Casuntaubadd CAnnhAtOYNTO0У L7inu co moy to.voiServiceTesteeendhles orchwerwicee+0.destsrunxes drvkees/arm/S bestore Servtcer/st.php -f itervetest/brsecontact' 2562PHPUnit 11.5.55 by Sebastian Bergmann and contributors.contelouratciontnore/tinnyl ohounx"11/ 11 (100%Tase: 00:20.908, Menory: 60.00 мbAl 11 new and existing testParseContact* tests pass. The deprecations are pre-existing PHPUnit warningsLet ma aico ar'cklv nin tha mil SarufcaTast to contimm nothioo ie beokaa.vendor/bin/phpun:PHPUnzt 11.5.55 by Sebastzan Beramann and contributors.OHp SH1RlA ARoNecant alll+ < Code$AdaptiveotterencePurtent vereionnuhibictunctaion testParsecontactPhonelchValsidkunber.orwosdSservice = Sthis-›getServiceMock@:ScrmData = ['Phone' => *[PHONE]*]SparsedNumber, Sext) = self::invokePrivateMethod("parseContactPhone', Sservice, null, ScrnData):Sthis->assertArrayHasKey("phone', SparsedNumber)Sthis->assentSane(*[PHONE]', SparsedNumber i"phone'):SihlorsoodontMmn(Cout)public function testParseContactPhoneWithExtension: voidSservice = Sthis->getServiceMockO=SoneData = f'Phone" =>*[PHONE] ext. 42'1.fSoansedNunben. Sextl = self:sinvokePoivatekethod("pansetontactPhone" Sservice, F'US' SccelatabeSthis->assertArrayHaskey("phone", Sparsedwunber)hsosnsser SanelW seyoixed = nuli), [value: mixed = null), [boolean: string = 'and)]KModturThsmerkerwhiest4 space...
|
NULL
|
-2102957735126746291
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhpStormvewNeweNNCCoocWindowFV faVsco.|s ~#12121 o PhpStormvewNeweNNCCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-inwnapers.oh©ActivityControlier.phpuewholeooserver.ono Tesmuonsc) eienupnp>E HubspolE IntcaratonAoe>E Listener:class ServiceTest extends TestCasev saestorch› E FieldsOpoorunityMatcherOpportunitvSvncStrate26372638s-Saccentlor.matscesu.rSthis->assertLessThan0rEqual( maximum: 25, strlen(Sresult)):2648› ProspectsearchStrategServicetraits©elientrest.php© DecorateActivityTest.p 2643po ated, oestn tteshublic function testParseContactMob{leWlithCountryCode: voicSservaee - sunasosgerseracenocklshScreData = "MobilePhone' => *0791112345691#© FieldDetinitionsTest.phCCeTcwmaet ame#i PavtoadRuilderTest.ohehut nvokernVethttnod"parseContactMobile', Sservice. ('GB", Scrabatal"Quer Rt dertest nhnQuervHandierTast ooduSoassenorOrKorUROuerviteratorTast mhousorassenorn hostar s thl ecuetewssune© QueryResultsTests.php© ServiceTest.php© SyncBatchRedisServickRacASanicatast obopublic functiion testinport@pportunitySkiosilhenProfileNotFoundO: voidi...:4 lof2odtdXReM F OXG• Changes 5 files, updating*^L+→f Side-by-side viewer -841e8c6b5 tests/Unit/ServiceDo not ignore"© JiminnyDebugCommand.php app/Console/CommandsCnocnllt o ColtinuAhoon ustolothadld nancorantadtohano"pe loagina.ono contia@ PlaybackService.pho app/ServicesSthis-›assertSame(Sexpected, Sresulte servicetestonoyUnversioned filce 9 flas, uodatina.E.enw.nikilocal.aodpublic static function parseContactMobileProvider@: arrayeenw.other aog@ GanAccossA ReporteTest.oho teste/Unit/Policiosreturn ienocy hoozlernone recurns null'=> (['MobilePhone' => **1. null, nulz]createWlock.skiminny?eoot?ecteommandlonoaog/conso.e/commandsintu favicon.ico publicsldk.yandR.raw.sal.querv.sol.aoolaserynoae tniooesernechockioS mulateWebhookscommand.oho.ano/Concole/Commandis/Crm/Hubsoottresulsser.nvoke?owrche.hooldparertontcicrhooktrsenutce.countnweooe.scomihcl1sSthisonssentSane(Scxoccted, Sresult):M. WERHOOK FILTFRING IMDIFMENTATION md anrnunide tuncciontesairhor tonortunsysk nswhenProror ellor Soundioie wose=custom.logaraveuesA SF fiminny@localhost)HS Jocal (jminny@localhost)A console [STAGING17781TX AutowINTERVAL 38 DAYXbY 0.20u.caszlBY sns count DESCu.name,u.sorcphonenuaben* fron ceans where 20 =1& console (PROD) X# console fiaum045 A1 A41 У 66 4NCATCu,id. CASE WHEN u.id = thouner_id THEN • (oaner)* ELSE ** END) AS user_idormen fo FRil soctal accnuntssasers u on u.1d = sa.soclable_1cParGTnCoontd n.teandu.tean_1d = 1117 and sa.provider = "hubspot":*EROM actMtes WHEOF Wund to binl18a2tetth-2147-1817.0144-40480695926801= Musd: " 70033459 VES*EROM actM tes WHEOF mund to binl:255208/3-800/-1781-0276.66648288185c01= Musde 80186192 M0*EROM eon confaurazons MHEPF 3d = 1953%* FROM teans WHERE id = 1117:+Eoon licene whene a= 30240.X1€?Casuntaubadd CAnnhAtOYNTO0У L7inu co moy to.voiServiceTesteeendhles orchwerwicee+0.destsrunxes drvkees/arm/S bestore Servtcer/st.php -f itervetest/brsecontact' 2562PHPUnit 11.5.55 by Sebastian Bergmann and contributors.contelouratciontnore/tinnyl ohounx"11/ 11 (100%Tase: 00:20.908, Menory: 60.00 мbAl 11 new and existing testParseContact* tests pass. The deprecations are pre-existing PHPUnit warningsLet ma aico ar'cklv nin tha mil SarufcaTast to contimm nothioo ie beokaa.vendor/bin/phpun:PHPUnzt 11.5.55 by Sebastzan Beramann and contributors.OHp SH1RlA ARoNecant alll+ < Code$AdaptiveotterencePurtent vereionnuhibictunctaion testParsecontactPhonelchValsidkunber.orwosdSservice = Sthis-›getServiceMock@:ScrmData = ['Phone' => *[PHONE]*]SparsedNumber, Sext) = self::invokePrivateMethod("parseContactPhone', Sservice, null, ScrnData):Sthis->assertArrayHasKey("phone', SparsedNumber)Sthis->assentSane(*[PHONE]', SparsedNumber i"phone'):SihlorsoodontMmn(Cout)public function testParseContactPhoneWithExtension: voidSservice = Sthis->getServiceMockO=SoneData = f'Phone" =>*[PHONE] ext. 42'1.fSoansedNunben. Sextl = self:sinvokePoivatekethod("pansetontactPhone" Sservice, F'US' SccelatabeSthis->assertArrayHaskey("phone", Sparsedwunber)hsosnsser SanelW seyoixed = nuli), [value: mixed = null), [boolean: string = 'and)]KModturThsmerkerwhiest4 space...
|
85762
|
NULL
|
NULL
|
NULL
|
|
85762
|
2941
|
4
|
2026-05-28T13:08:20.245763+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973700245_m2.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Built-in Preview
Chrome
Firefox
Safari...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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.85638297,"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":"ServiceTest","depth":6,"bounds":{"left":0.87167555,"top":0.019952115,"width":0.043882977,"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 'ServiceTest'","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 'ServiceTest'","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":"Built-in Preview","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":"Chrome","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":"Firefox","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":"Safari","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}]...
|
8099280038376358662
|
-9066624220847174716
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Built-in Preview
Chrome
Firefox
Safari
PhpStormViewNeweNNCCodeKelucioWindowFV faVsco.|s ~#12121 on JY-20963-fx-lproideteSarvicc.ohhe bolbees.phe©ActivityControlier.php• HttpIntegrationsnlinteractionsuserRo cobserver.onC) Team.phpo Nemeuoni(© Client.phpJobsclass ServiceTest extends TestCasepubuzc tunccion cescrarseconcacchoozle struncaredlozstnars. vorea MailiModelo2637s-Saccentlor.matscesu.r0 Notifications2639voseivers2648© SocialAccountobservertescy 2641UserObserverTest.phpUserRoleObserverTest.pho(tn Policies2643Sthis->assertLessThan0rEqual( maximum: 25, strlen(Sresult)):ouhke function tactPanseContactMohflelkthCounteufodeme wosaa Providerla RepositoriesSservaee - sunasosgerseracenocklshScreData = "MobilePhone' => *0791112345691#E Ruc.Ea Services2649264626472649hut nvokernVethttnod"parseContactMobile', Sservice. ('GB", Scrabatal>EACtVIYha ActivityProvidersuSoassenorOrKorUR2650uu sorassenorw nostar wothitechus@ Calendarleonterence2652public functiion testinport@pportunitySkiosilhenProfileNotFoundO: voidi...:a erm→Rulihor"lot2edtChanges 5 filesE.env.local apc© JiminnyDebugCommand.php app/Console/CommandsT+ 0 +→0 Side-by-side viewer -@ 41e8c6bS app/Services/PlaybackService.phpDo not ignore"Highlight words -x15€pe loagina.ono contia© Paybickservice.ono apo servicee@ ServiceTest.pho tests/Unit/Services/Crm/SalesforceStean & Stracko>oe Activitye ->oetUser Or>aetteam oryUnversoncd SiloesneE.enw.nikilocal andE enw.other and@ GanAccossA ReporteTest.oho teste/Unit/PoliciosEnumonsto and doloto l connonteforeach (Splaylist "mediaSegnents') as Ssegment) ?Ssegment ""uri' = client cdn(Ssegnent ("uri'), Stean):(c ereate lock.sk iminny ?eoot cest eommandlono angfeonsa elcommandslntu favicon.ico publicsldk.yandR.raw.sol.querv.sol aodSplayList ("EXT-X-PLAYLIST-TYPE' = 'VOD'ASplayList (*EXT-X-ENDLIST' = true:S mulateWebhookscommand.oho.ano/.Concole/Commandis/crm/HubsootM.WERHOOK FILTERING. IMPIFMENTATION md and=custom.logaraveuesA SF fiminny@localhost)& console (PROD) XA console [STAGINGTX AutowINTERVAL 38 DAY.bY 0.20u.caszl17781BY sns count DESC* fron ceans where 20 =1NCATCu,id. CASE WHEN u.id = thouner_id THEN • (oaner)* ELSE ** END) AS user_idena1aormen fo FRil soctal accnuntssasers u on u.1d = sa.soclable_1cPargTncaontd n.teandu.tean_1d = 1117 and sa.provider = "hubspot":*EROM actMtes WHEOF Wund to binl18a2tetth-2147-1817.0144-40480695926801= Musd: " 70033459 VES*EROM ActM tes WHEOF muhd to binl:255208/3-800/-1781-0276.66648288185c01= musde 80186192 M0* EROM eon confqurazons MHERF 3d = 1953%* FROM teans WHERE id = 1117:+Eoon usene whene3d=30240.# console fiaum8458185Y00TO0У L7Inu co moy to.voilServiceTesteeendhles orchwerwicee+0.destsrunxes drvkees/arm/S bestore Servtcer/st.php -f itervetest/brsecontact' 2562PHPUnit 11.5.55 by Sebastian Bergmann and contributors.contelourarcion.Tno.e/uminnyl chounx=11/ 11 (100%Тдоe: 00:10.908, Меnогy: 60.00 мбAl 11 new and existing testParseContact* tests pass. The deprecations are pre-existing PHPUnit warningsLet ma aico ar'cklv nin tha mil SarufcaTast to contimm nothioo ie beokaa.vendor/bin/phpuniPHPUnzt 11.5.55 by Sebastzan Beramann and contributors.RuntrOHp SH1RlA ARoNecant alll+ < Code§ AdaptiveditterencePurtent vereionyServices> PaybackService > cenerateMedaPiiAAAAASteam = Strack->aetActvitylor>aerlser Oo>oeteaniobEnumerate and delete all segments.foreach (Splaylist 'mediaSegments' as Ssegment)Ssegnent ("uri') = client con(Ssegment("uri', Stean):SplayList("EXT-X-PLAYLIST-TYPE') = *VOD*:SplayList("EXT-X-ENDLIST') = true:N Wodtur Teams DRARR UTE-R AIA enano...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85745
|
2939
|
13
|
2026-05-28T13:04:43.209438+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973483209_m2.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Built-in Preview
Chrome
Firefox
Safari
Sync Changes
Hide This Notification
Code changed:
Hide
3
32
81
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Salesforce;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Facades\Cache;
use Jiminny\Component\Country\CountriesMap;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Enums\CrmObject;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\NoResultsException;
use Jiminny\Integrations\PlaybookResolver;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Playbook;
use Jiminny\Models\Team;
use Jiminny\Models\TeamSettings;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Salesforce\Client;
use Jiminny\Services\Crm\Salesforce\PayloadBuilder;
use Jiminny\Services\Crm\Salesforce\QueryBuilder;
use Jiminny\Services\Crm\Salesforce\QueryHandler;
use Jiminny\Services\Crm\Salesforce\QueryIterator;
use Jiminny\Services\Crm\Salesforce\QueryResults;
use Jiminny\Services\Crm\Salesforce\Service;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
use Tests\Unit\Traits\TestPrivateMethod;
class ServiceTest extends TestCase
{
use TestPrivateMethod;
public function testFetchAndAssociateRelatedActivity(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$payloadBuilder->method('addCustomLogicFieldsPayload')
->willReturnCallback(function ($activity, $payload) {
return $payload;
});
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods(['fetchRelatedActivity', 'getPlaybook', 'getPlaybookCategory', 'updateRecord'])
->getMock();
$serviceMock->expects($this->once())
->method('fetchRelatedActivity')
->willReturn([
'Id' => 'testId',
'Type' => null,
'OwnerId' => 'testerUser',
'Description' => 'Test description',
]);
$user = $this->createMock(User::class);
$team = $this->createMock(Team::class);
$user->method('getAttribute')->with('team')->willReturn($team);
$playbook = $this->createMock(Playbook::class);
$playbook->method('getActivityField')->willReturn(null);
$playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_EVENT);
$serviceMock->expects($this->once())
->method('getPlaybook')
->with($user)
->willReturn($playbook);
$serviceMock->expects($this->never())
->method('getPlaybookCategory');
$serviceMock->expects($this->never())
->method('updateRecord');
$fieldDataRepository = $this->createMock(FieldDataRepository::class);
$fieldDataRepository->method('getActivityFieldData')->willReturn(collect([]));
app()->instance(FieldDataRepository::class, $fieldDataRepository);
$config = $this->createMock(Configuration::class);
$profilesRelation = $this->getMockBuilder(\Illuminate\Database\Eloquent\Relations\HasMany::class)
->disableOriginalConstructor()
->onlyMethods(['get'])
->addMethods(['where'])
->getMock();
$profilesRelation->method('where')->willReturnSelf();
$profilesRelation->method('get')->willReturn(collect([]));
$config->method('profiles')->willReturn($profilesRelation);
$serviceMock->config = $config;
$serviceMock->profile = null;
$actualStartTime = \Carbon\Carbon::now();
$activity = $this->getMockBuilder(Activity::class)
->disableOriginalConstructor()
->onlyMethods(['update', 'hasProspect'])
->getMock();
$activity->method('update')->willReturn(true);
$activity->method('hasProspect')->willReturn(true);
$activity->type = Activity::TYPE_CONFERENCE;
$activity->provider = Activity::PROVIDER_TWILIO;
$activity->lead_id = 1;
$activity->user_id = 0;
$activity->id_string = 'test-activity-id';
$activity->user = $user;
$activity->actual_start_time = $actualStartTime;
$activity->uuid = 'c53d8320-f556-4cee-a2f8-5f232f454ca4';
app()->bind(PlaybookResolver::class, function () use ($user) {
$playbook = $this->createMock(Playbook::class);
$playbookResolver = $this->createMock(PlaybookResolver::class);
$playbookResolver->expects($this->once())
->method('resolvePlaybookByUser')
->with($user)
->willReturn($playbook);
return $playbookResolver;
});
$data = $serviceMock->fetchAndAssociateRelatedActivity($activity);
$this->assertInstanceOf(Activity::class, $data);
$this->assertEquals(Activity::TYPE_CONFERENCE, $data->getType());
$this->assertEquals($actualStartTime->getTimestamp(), $data->getActualStartTime()->getTimestamp());
}
public function testFetchAndAssociateRelatedActivitySkipsForTaskBasedPlaybook(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods(['fetchRelatedActivity', 'getPlaybook'])
->getMock();
$user = $this->createMock(User::class);
$playbook = $this->createMock(Playbook::class);
$playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);
$playbook->method('getId')->willReturn(123);
$serviceMock->expects($this->once())
->method('getPlaybook')
->with($user)
->willReturn($playbook);
$serviceMock->expects($this->never())
->method('fetchRelatedActivity');
$activity = $this->getMockBuilder(Activity::class)
->disableOriginalConstructor()
->onlyMethods(['hasProspect', 'getUuid'])
->getMock();
$activity->method('hasProspect')->willReturn(true);
$activity->method('getUuid')->willReturn('c53d8320-f556-4cee-a2f8-5f232f454ca4');
$activity->type = Activity::TYPE_CONFERENCE;
$activity->actual_start_time = \Carbon\Carbon::now();
$activity->user = $user;
$result = $serviceMock->fetchAndAssociateRelatedActivity($activity);
$this->assertNull($result);
}
public function testFetchAndAssociateRelatedActivityReturnsNullForNonConference(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
$serviceMock = new Service(
client: $client,
payloadBuilder: $payloadBuilder,
eventDispatcher: $eventDispatcher,
countriesMap: $countriesMap,
prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class)
);
$activity = $this->getMockBuilder(Activity::class)
->disableOriginalConstructor()
->getMock();
$activity->type = Activity::TYPE_SOFTPHONE;
$result = $serviceMock->fetchAndAssociateRelatedActivity($activity);
$this->assertNull($result);
}
public function testFetchAndAssociateRelatedActivityReturnsNullWhenNoStartTime(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
$serviceMock = new Service(
client: $client,
payloadBuilder: $payloadBuilder,
eventDispatcher: $eventDispatcher,
countriesMap: $countriesMap,
prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class)
);
$activity = $this->getMockBuilder(Activity::class)
->disableOriginalConstructor()
->getMock();
$activity->type = Activity::TYPE_CONFERENCE;
$activity->actual_start_time = null;
$activity->scheduled_start_time = null;
$result = $serviceMock->fetchAndAssociateRelatedActivity($activity);
$this->assertNull($result);
}
public function testFetchAndAssociateRelatedActivityReturnsNullWhenNoProspect(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods(['getPlaybook'])
->getMock();
$serviceMock->expects($this->never())
->method('getPlaybook');
$activity = $this->getMockBuilder(Activity::class)
->disableOriginalConstructor()
->onlyMethods(['hasProspect', 'getUuid'])
->getMock();
$activity->method('hasProspect')->willReturn(false);
$activity->method('getUuid')->willReturn('c53d8320-f556-4cee-a2f8-5f232f454ca4');
$activity->type = Activity::TYPE_CONFERENCE;
$activity->actual_start_time = \Carbon\Carbon::now();
$result = $serviceMock->fetchAndAssociateRelatedActivity($activity);
$this->assertNull($result);
}
public function testMatchExactlyByEmail(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods([])
->getMock();
$profile = new Profile();
$profile->setAttribute('id', bin2hex(random_bytes(8)));
$serviceMock->profile = $profile;
$team = $this->createMock(Team::class);
$serviceMock->team = $team;
$data = $serviceMock->matchExactlyByEmail(bin2hex(random_bytes(8)) . '[EMAIL]');
$this->assertEquals(null, $data);
}
public function testMatchDomainFromEmail(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$queryIterator = $this->createMock(QueryIterator::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
$config = $this->createMock(Configuration::class);
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->any())
->method('search')
->willReturn($queryIterator);
return $handler;
});
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods(['convertCrmData'])
->getMock();
$profile = new Profile();
$profile->account_fields = 'Field1, Field2, Field3';
$serviceMock->profile = $profile;
$serviceMock->expects($this->once())
->method('convertCrmData')
->willReturn(['test']);
$this->app->bind(QueryBuilder::class, function () {
$queryBuilder = $this->createMock(QueryBuilder::class);
$queryBuilder->expects($this->once())
->method('buildMatchByDomainQuery')
->with('[EMAIL]')
->willReturn('FIND {[EMAIL]} IN ALL FIELDS RETURNING Account(Id)');
return $queryBuilder;
});
$team = $this->createMock(Team::class);
$serviceMock->team = $team;
$serviceMock->config = $config;
$data = $serviceMock->matchByDomain('[EMAIL]');
$this->assertEquals(['test'], $data);
}
public function testBuildTaskSearchFields(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
$service = new Service(
client: $client,
payloadBuilder: $payloadBuilder,
eventDispatcher: $eventDispatcher,
countriesMap: $countriesMap,
prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class)
);
$fields = $service->buildTaskSearchFields();
$expectedFields = ['Id', 'WhoId', 'WhatId', 'AccountId'];
$this->assertEquals($expectedFields, $fields);
}
public function testMapCrmObjects(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
$service = new Service(
client: $client,
payloadBuilder: $payloadBuilder,
eventDispatcher: $eventDispatcher,
countriesMap: $countriesMap,
prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class),
);
$sampleTask = [
'WhoId' => '003sampleWhoId',
'AccountId' => 'sampleAccountId',
'WhatId' => 'sampleWhatId',
];
$activityData = $service->mapCrmObjects($sampleTask);
$expectedActivityData = [
'contact' => '003sampleWhoId',
'account' => 'sampleAccountId',
'opportunity' => 'sampleWhatId',
];
$this->assertEquals($expectedActivityData, $activityData);
}
public function testGetInstalledAppVersion(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
$queryIterator = $this->createMock(QueryIterator::class);
$queryIterator->expects($this->any())
->method('current')->willReturn([
'SubscriberPackageVersion' => [
'MajorVersion' => '1',
'MinorVersion' => '0',
'PatchVersion' => '1',
'BuildNumber' => '0',
],
]);
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->any())
->method('metadata')
->willReturn($queryIterator);
return $handler;
});
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods(array_diff(get_class_methods(Service::class), ['getInstalledAppVersion']))
->getMock();
$version = $serviceMock->getInstalledAppVersion();
$this->assertEquals('1010', $version);
}
public function testSyncProfiles(): void
{
$userToSearch = null;
$team = $this->createMock(Team::class);
$config = $this->createMockedConfiguration();
$config->expects($this->once())
->method('getId')
->willReturn(1);
$salesforceUser = [
'Email' => '[EMAIL]',
'UserPreferencesLightningExperiencePreferred' => true,
'CallCenterId' => '123',
'Id' => '456',
'ProfileId' => '789',
];
app()->bind(QueryBuilder::class, function () use ($userToSearch) {
$queryBuilder = $this->createMock(QueryBuilder::class);
$queryBuilder->expects($this->once())
->method('buildGetUsersQuery')
->with($userToSearch)
->willReturn('SELECT * FROM Users');
return $queryBuilder;
});
$queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults([$salesforceUser], 1, true, null));
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->any())
->method('query')
->with('SELECT * FROM Users')
->willReturn($queryIterator);
return $handler;
});
$user = $this->createMock(User::class);
$user->expects($this->once())
->method('getId')
->willReturn(123);
$this->mockTeamRepository($team, $salesforceUser, $user);
$profileRepository = $this->createMock(ProfileRepository::class);
$profileRepository->expects($this->once())
->method('updateOrCreateProfile')
->with(
$user,
[
'crm_configuration_id' => 1,
'crm_provider_id' => '456',
],
[
'user_id' => 123,
'edition' => Profile::EDITION_LIGHTNING,
'has_external_cti' => true,
'crm_profile_id' => '789',
]
)
->willReturn(new Profile());
$this->app->instance(ProfileRepository::class, $profileRepository);
$serviceMock = $this->getServiceMock();
$serviceMock->team = $team;
$serviceMock->config = $config;
$result = $serviceMock->syncProfiles($userToSearch);
$this->assertNull($result);
}
public function testSyncProfilesEmailIsNull(): void
{
$userToSearch = $this->createMock(User::class);
$salesforceUser = [
'Email' => null,
];
app()->bind(QueryBuilder::class, function () {
$queryBuilder = $this->createMock(QueryBuilder::class);
$queryBuilder->expects($this->once())
->method('buildGetUsersQuery')
->with(null)
->willReturn('SELECT * FROM Users');
return $queryBuilder;
});
$queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults([$salesforceUser], 1, true, null));
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->exactly(2))
->method('query')
->willReturn($queryIterator);
return $handler;
});
$team = $this->createMock(Team::class);
$user = $this->createMock(User::class);
$this->mockTeamRepository($team, $salesforceUser, $user, false);
$config = $this->createMock(Configuration::class);
$serviceMock = $this->getServiceMock();
$serviceMock->team = $team;
$serviceMock->config = $config;
$profile = $serviceMock->syncProfiles(null);
$this->assertNull($profile);
}
public function testSyncProfilesUserToSearchMatchesCurrentUser(): void
{
$userToSearch = $this->createMock(User::class);
$userToSearch->expects($this->once())
->method('getId')
->willReturn(123);
$team = $this->createMock(Team::class);
$config = $this->createMock(Configuration::class);
$config->expects($this->once())
->method('getId')
->willReturn(1);
$salesforceUser = [
'Email' => '[EMAIL]',
'UserPreferencesLightningExperiencePreferred' => true,
'CallCenterId' => '123',
'Id' => '456',
'ProfileId' => '789',
];
app()->bind(QueryBuilder::class, function () use ($userToSearch) {
$queryBuilder = $this->createMock(QueryBuilder::class);
$queryBuilder->expects($this->once())
->method('buildGetUsersQuery')
->with($userToSearch)
->willReturn('SELECT * FROM Users');
return $queryBuilder;
});
$queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults([$salesforceUser], 1, true, null));
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->any())
->method('query')
->with('SELECT * FROM Users')
->willReturn($queryIterator);
return $handler;
});
$user = $this->createMock(User::class);
$user->expects($this->exactly(2))
->method('getId')
->willReturn(123);
$this->mockTeamRepository($team, $salesforceUser, $user);
$profileRepository = $this->createMock(ProfileRepository::class);
$profileRepository->expects($this->once())
->method('updateOrCreateProfile')
->with(
$user,
[
'crm_configuration_id' => 1,
'crm_provider_id' => '456',
],
[
'user_id' => 123,
'edition' => Profile::EDITION_LIGHTNING,
'has_external_cti' => true,
'crm_profile_id' => '789',
]
)
->willReturn(new Profile());
$this->app->instance(ProfileRepository::class, $profileRepository);
$serviceMock = $this->getServiceMock();
$serviceMock->team = $team;
$serviceMock->config = $config;
$profile = $serviceMock->syncProfiles($userToSearch);
$this->assertInstanceOf(Profile::class, $profile);
}
public function testSyncProfilesWithCustomValidation(): void
{
$userToSearch = null;
$team = $this->createMock(Team::class);
$config = $this->createMockedConfiguration();
$config->expects($this->atLeastOnce()) // Changed from once() to atLeastOnce()
->method('getId')
->willReturn(1);
$salesforceUser = [
'Email' => '[EMAIL]',
'UserPreferencesLightningExperiencePreferred' => true,
'CallCenterId' => '123',
'Id' => '456',
'ProfileId' => '789',
'CustomField' => 'CustomValue',
];
$customRules = [
['field' => 'CustomField', 'value' => 'CustomValue'],
];
$this->mockQueryBuilderAndHandler($userToSearch, [$salesforceUser]);
$user = $this->createMock(User::class);
$user->expects($this->once())
->method('getId')
->willReturn(123);
$this->mockTeamRepository($team, $salesforceUser, $user, true, $customRules);
$this->mockProfileRepository($user);
$serviceMock = $this->getServiceMock();
$serviceMock->team = $team;
$serviceMock->config = $config;
$profile = $serviceMock->syncProfiles($userToSearch);
$this->assertNull($profile);
}
public function testSyncProfilesWithCustomValidationFailing(): void
{
$userToSearch = null;
$team = $this->createMock(Team::class);
$config = $this->createMockedConfiguration();
$salesforceUser = [
'Email' => '[EMAIL]',
'UserPreferencesLightningExperiencePreferred' => true,
'CallCenterId' => '123',
'Id' => '456',
'ProfileId' => '789',
'CustomField' => 'WrongValue',
];
$customRules = [
['field' => 'CustomField', 'value' => 'CustomValue'],
];
$this->mockQueryBuilderAndHandler($userToSearch, [$salesforceUser]);
$teamRepository = $this->getMockForAbstractClass(TeamRepository::class, [], '', false, true, true, ['findActiveTeamMemberByEmail', 'getTeamSetting']);
$teamSettings = $this->createMock(TeamSettings::class);
$teamSettings->method('getValueType')
->willReturn('array');
$teamSettings->method('getValue')
->willReturn(json_encode($customRules));
$teamRepository->expects($this->once())
->method('getTeamSetting')
->with($team, 'custom_profile_validation')
->willReturn($teamSettings);
app()->bind(TeamRepository::class, function () use ($teamRepository) {
return $teamRepository;
});
$profileRepository = $this->createMock(ProfileRepository::class);
$profileRepository->expects($this->never())
->method('updateOrCreateProfile');
$this->app->instance(ProfileRepository::class, $profileRepository);
$serviceMock = $this->getServiceMock();
$serviceMock->team = $team;
$serviceMock->config = $config;
$result = $serviceMock->syncProfiles($userToSearch);
$this->assertNull($result);
}
public function testGetContactRolesFromCrm(): void
{
$contactRoles = [
[
'Id' => '1',
'ContactId' => 'Contact1',
'OpportunityId' => 'Opportunity1',
'Opportunity' => ['OwnerId' => 'Owner1'],
'IsPrimary' => true,
'Role' => 'Decision Maker',
],
];
$expectedResponse = [
[
'id' => '1',
'contactId' => 'Contact1',
'opportunityId' => 'Opportunity1',
'ownerId' => 'Owner1',
'isPrimary' => true,
'role' => 'Decision Maker',
],
];
$this->bindQueryIterator($contactRoles);
$serviceMock = $this->getServiceMock();
$data = $serviceMock->getContactRolesFromCrm(now()->subDay());
$this->assertEquals($expectedResponse, $data);
}
public function testGetContactRolesFromCrmNoResult(): void
{
$this->bindQueryIterator([]);
$serviceMock = $this->getServiceMock();
$data = $serviceMock->getContactRolesFromCrm(now()->subDay());
$this->assertEquals([], $data);
}
public function testSyncContactRoles(): void
{
$contactRoles = [
[
'id' => '1',
'contactId' => 'Contact1',
'opportunityId' => 'Opportunity1',
'ownerId' => 'Owner1',
'isPrimary' => true,
'role' => 'Decision Maker',
],
];
app()->bind(ContactRoleRepository::class, function () {
$contactRoleRepository = $this->createMock(ContactRoleRepository::class);
$contactRoleRepository->expects($this->once())
->method('saveContactRoles');
return $contactRoleRepository;
});
$serviceMock = $this->getServiceMock([
'getContactRolesFromCrm',
'syncRemotelyDeletedContactRoles',
'syncContact',
'syncOpportunity',
]);
$config = $this->createMock(Configuration::class);
$hasMany = $this->createMock(HasManyExtended::class);
$hasMany->expects($this->exactly(2))
->method('where')
->willReturn($hasMany);
$hasMany->expects($this->exactly(2))
->method('first')
->willReturn(
$this->createMock(Contact::class),
$this->createMock(Opportunity::class)
);
$config->expects($this->once())
->method('contacts')
->willReturn($hasMany);
$config->expects($this->once())
->method('opportunities')
->willReturn($hasMany);
$serviceMock->config = $config;
$serviceMock->expects($this->once())
->method('getContactRolesFromCrm')
->willReturn($contactRoles);
$serviceMock->expects($this->once())
->method('syncRemotelyDeletedContactRoles');
$data = $serviceMock->syncContactRoles(now()->subDay());
$this->assertEquals(1, $data);
}
public function testSyncRemotelyDeletedContactRoles(): void
{
$contactRoles = [
[
'id' => '1',
'crm_provider_id' => '1',
],
];
app()->bind(QueryHandler::class, function () use ($contactRoles) {
$queryResults = new QueryResults($contactRoles, 1, true, null);
$handler = $this->createMock(QueryHandler::class);
$handler->method('queryDeleted')
->willReturn($queryResults);
return $handler;
});
app()->bind(ContactRoleRepository::class, function () {
$contactRoleRepository = $this->createMock(ContactRoleRepository::class);
$contactRoleRepository->expects($this->once())
->method('deleteContactRoles');
return $contactRoleRepository;
});
$serviceMock = $this->getServiceMock();
$serviceMock->team = $this->createMock(Team::class);
$data = $this->invokePrivateMethod('syncRemotelyDeletedContactRoles', $serviceMock, []);
$this->assertTrue($data);
}
private function bindQueryIterator(array $queryResult): void
{
/** @var Client $client */
$client = $this->createMock(Client::class);
$queryIterator = new QueryIterator(
$client,
new QueryResults($queryResult, 1, true, null)
);
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->any())
->method('query')
->willReturn($queryIterator);
return $handler;
});
}
public static function getOpportunitySortOrderDataProvider(): array
{
return [
'all open recently updated' => [Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED, ['LastModifiedDate DESC', true]],
'all open recently created' => [Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED, ['CreatedDate DESC', true]],
'all open oldest created' => [Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED, ['CreatedDate ASC', true]],
'all recently updated' => [Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED, ['LastModifiedDate DESC', false]],
'default' => ['unknown', ['LastModifiedDate DESC', true]],
];
}
private function createMockedConfiguration(): Configuration
{
$config = $this->createMock(Configuration::class);
$profilesRelation = $this->getMockBuilder(\Illuminate\Database\Eloquent\Relations\HasMany::class)
->disableOriginalConstructor()
->onlyMethods(['get'])
->addMethods(['where', 'first'])
->getMock();
$profilesRelation->method('where')->willReturnSelf();
$profilesRelation->method('get')->willReturn(collect([]));
$profilesRelation->method('first')->willReturn(null);
$config->method('profiles')->willReturn($profilesRelation);
return $config;
}
private function getServiceMock(array $onlyMethods = []): MockObject&Service
{
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$this->createMock(Client::class),
$this->createMock(PayloadBuilder::class),
$this->createMock(Dispatcher::class),
$this->createMock(CountriesMap::class),
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods($onlyMethods)
->getMock();
$serviceMock->profile = $this->createMock(Profile::class);
return $serviceMock;
}
private function mockQueryBuilderAndHandler($userToSearch, $salesforceUsers): void
{
app()->bind(QueryBuilder::class, function () use ($userToSearch) {
$queryBuilder = $this->createMock(QueryBuilder::class);
$queryBuilder->expects($this->once())
->method('buildGetUsersQuery')
->with($userToSearch)
->willReturn('SELECT * FROM Users');
return $queryBuilder;
});
$queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults($salesforceUsers, count($salesforceUsers), true, null));
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->any())
->method('query')
->willReturn($queryIterator);
return $handler;
});
}
private function mockTeamRepository(
Team $team,
array $salesforceUser,
?User $user = null,
bool $userSearch = true,
array $customRules = []
): void {
$teamRepository = $this->getMockForAbstractClass(TeamRepository::class, [], '', false, true, true, ['findActiveTeamMemberByEmail', 'getTeamSetting']);
if ($userSearch) {
$teamRepository->expects($this->once())
->method('findActiveTeamMemberByEmail')
->with($team, $salesforceUser['Email'])
->willReturn($user);
}
$teamSettings = $this->createMock(TeamSettings::class);
$teamSettings->method('getValueType')
->willReturn('array');
$teamSettings->method('getValue')
->willReturn(json_encode($customRules));
$teamRepository->expects($this->once())
->method('getTeamSetting')
->with($team, 'custom_profile_validation')
->willReturn($teamSettings);
app()->bind(TeamRepository::class, function () use ($teamRepository) {
return $teamRepository;
});
}
private function mockProfileRepository(User $user): void
{
$profileRepository = $this->createMock(ProfileRepository::class);
$profileRepository->expects($this->once())
->method('updateOrCreateProfile')
->with(
$user,
[
'crm_configuration_id' => 1,
'crm_provider_id' => '456',
],
[
'user_id' => 123,
'edition' => Profile::EDITION_LIGHTNING,
'has_external_cti' => true,
'crm_profile_id' => '789',
]
)
->willReturn(new Profile());
$this->app->instance(ProfileRepository::class, $profileRepository);
}
public function testBuildEnhancedNoteDecodesHtmlEntities(): void
{
$service = $this->getServiceMock(['createRecord']);
$profile = new Profile();
$profile->setAttribute('crm_provider_id', 'owner-123');
$service->profile = $profile;
$service->expects($this->exactly(2))
->method('createRecord')
->willReturnOnConsecutiveCalls('note-id-123', 'link-id-456');
$bodyWithEntities = 'Welch's current challenges and Facebook's Club';
$result = $this->invokePrivateMethod('buildEnhancedNote', $service, [
'Test Title',
$bodyWithEntities,
'object-id-789',
]);
$this->assertEquals('note-id-123', $result);
}
public function testBuildEnhancedNoteSanitizesWithoutQuotes(): void
{
$service = $this->getServiceMock(['createRecord']);
$profile = new Profile();
$profile->setAttribute('crm_provider_id', 'owner-456');
$service->profile = $profile;
$service->expects($this->exactly(2))
->method('createRecord')
->willReturnCallback(function ($type, $data) {
if ($type === 'ContentNote') {
$decoded = base64_decode($data['Content']);
$this->assertStringContainsString("Welch's", $decoded);
$this->assertStringNotContainsString(''', $decoded);
$this->assertStringNotContainsString('&#039;', $decoded);
$this->assertStringContainsString('<script>', $decoded);
return 'note-id-456';
}
return 'link-id-789';
});
$bodyWithMixedContent = "Welch's and <script>alert('xss')</script>";
$result = $this->invokePrivateMethod('buildEnhancedNote', $service, [
'Test Title',
$bodyWithMixedContent,
'object-id-123',
]);
$this->assertEquals('note-id-456', $result);
}
public function testBuildEnhancedNoteConvertsLineBreaks(): void
{
$service = $this->getServiceMock(['createRecord']);
$profile = new Profile();
$profile->setAttribute('crm_provider_id', 'owner-789');
$service->profile = $profile;
$service->expects($this->exactly(2))
->method('createRecord')
->willReturnCallback(function ($type, $data) {
if ($type === 'ContentNote') {
$decoded = base64_decode($data['Content']);
$this->assertStringContainsString('<br>', $decoded);
$this->assertStringNotContainsString('<br />', $decoded);
return 'note-id-789';
}
return 'link-id-012';
});
$bodyWithLineBreaks = "Line 1\nLine 2\nLine 3";
$result = $this->invokePrivateMethod('buildEnhancedNote', $service, [
'Test Title',
$bodyWithLineBreaks,
'object-id-456',
]);
$this->assertEquals('note-id-789', $result);
}
public function testBuildEnhancedNoteHandlesComplexScenario(): void
{
$service = $this->getServiceMock(['createRecord']);
$profile = new Profile();
$profile->setAttribute('crm_provider_id', 'owner-complex');
$service->profile = $profile;
$service->expects($this->exactly(2))
->method('createRecord')
->willReturnCallback(function ($type, $data) {
if ($type === 'ContentNote') {
$decoded = base64_decode($data['Content']);
$this->assertStringContainsString("Welch's", $decoded);
$this->assertStringContainsString("Facebook's Club", $decoded);
$this->assertStringContainsString("Arctics'", $decoded);
$this->assertStringNotContainsString(''', $decoded);
$this->assertStringNotContainsString('&#039;', $decoded);
$this->assertStringContainsString('<br>', $decoded);
$this->assertStringContainsString('<', $decoded);
$this->assertStringContainsString('>', $decoded);
return 'note-complex';
}
return 'link-complex';
});
$complexBody = "Summary:\n---------\nThe call focused on understanding Welch's current challenges and exploring how Arctics' Revenue Growth Management solutions could support their strategic goals.\n\n• John SMith discussed his role as a category advisor for Google and Facebook's Club, emphasizing the importance of market research and advising on product assortment.\n• Madona introduced Arctics' Virtual Shoppers AI, which simulates consumer <behavior> to optimize pricing and promotional strategies.";
$result = $this->invokePrivateMethod('buildEnhancedNote', $service, [
'Jiminny Transcription Summary',
$complexBody,
'task-id-001',
]);
$this->assertEquals('note-complex', $result);
}
public function testSyncRemotelyDeletedObjectsWithErrorHandlingSuccess(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$prospectPhotoPathService,
])
->onlyMethods(['syncRemotelyDeletedObjects'])
->getMock();
// Mock team
$team = $this->createMock(Team::class);
$team->method('getUuid')->willReturn('team-uuid-123');
$serviceMock->team = $team;
// Expect syncRemotelyDeletedObjects to be called once and succeed
$serviceMock->expects($this->once())
->method('syncRemotelyDeletedObjects')
->with(\Jiminny\Enums\CrmObject::ACCOUNT);
// Call the protected method using reflection
$reflection = new \ReflectionClass($serviceMock);
$method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');
$method->setAccessible(true);
// Should not throw any exceptions
$method->invoke($serviceMock, \Jiminny\Enums\CrmObject::ACCOUNT);
$this->assertTrue(true); // Test completed successfully
}
public function testSyncRemotelyDeletedObjectsWithErrorHandlingFailure(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$prospectPhotoPathService,
])
->onlyMethods(['syncRemotelyDeletedObjects'])
->getMock();
// Mock team
$team = $this->createMock(Team::class);
$team->method('getUuid')->willReturn('team-uuid-456');
$serviceMock->team = $team;
// Mock logger to verify warning is logged
$logger = $this->createMock(\Psr\Log\LoggerInterface::class);
// Use reflection to set the protected logger property
$reflection = new \ReflectionClass($serviceMock);
$loggerProperty = $reflection->getProperty('logger');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($serviceMock, $logger);
$exception = new \Exception('Sync failed due to API error');
// Expect syncRemotelyDeletedObjects to throw an exception
$serviceMock->expects($this->once())
->method('syncRemotelyDeletedObjects')
->with(\Jiminny\Enums\CrmObject::CONTACT)
->willThrowException($exception);
// Expect warning to be logged with correct message and parameters
$logger->expects($this->once())
->method('warning')
->with(
'[Salesforce] Remotely deleted objects sync failed',
[
'objectType' => 'contact',
'teamId' => 'team-uuid-456',
'reason' => 'Sync failed due to API error',
]
);
// Call the protected method using reflection
$reflection = new \ReflectionClass($serviceMock);
$method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');
$method->setAccessible(true);
// Should not re-throw the exception, just log it
$method->invoke($serviceMock, \Jiminny\Enums\CrmObject::CONTACT);
$this->assertTrue(true); // Test completed successfully
}
public function testSyncRemotelyDeletedObjectsWithErrorHandlingWithLogParams(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$prospectPhotoPathService,
])
->onlyMethods(['syncRemotelyDeletedObjects'])
->getMock();
// Mock team
$team = $this->createMock(Team::class);
$team->method('getUuid')->willReturn('team-uuid-789');
$serviceMock->team = $team;
// Mock logger to verify warning is logged
$logger = $this->createMock(\Psr\Log\LoggerInterface::class);
// Use reflection to set the protected logger property
$loggerReflection = new \ReflectionClass($serviceMock);
$loggerProperty = $loggerReflection->getProperty('logger');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($serviceMock, $logger);
$exception = new \Exception('Network timeout');
// Expect syncRemotelyDeletedObjects to throw an exception
$serviceMock->expects($this->once())
->method('syncRemotelyDeletedObjects')
->with(\Jiminny\Enums\CrmObject::OPPORTUNITY)
->willThrowException($exception);
// Additional log parameters
$logParams = [
'syncType' => 'full',
'batchSize' => 100,
];
// Expect warning to be logged with merged parameters
$logger->expects($this->once())
->method('warning')
->with(
'[Salesforce] Remotely deleted objects sync failed',
[
'objectType' => 'opportunity',
'teamId' => 'team-uuid-789',
'reason' => 'Network timeout',
'syncType' => 'full',
'batchSize' => 100,
]
);
// Call the protected method using reflection
$reflection = new \ReflectionClass($serviceMock);
$method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');
$method->setAccessible(...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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.85638297,"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":"ServiceTest","depth":6,"bounds":{"left":0.87167555,"top":0.019952115,"width":0.043882977,"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 'ServiceTest'","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 'ServiceTest'","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":"Built-in Preview","depth":4,"bounds":{"left":0.35904256,"top":0.15562649,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chrome","depth":4,"bounds":{"left":0.36768618,"top":0.15562649,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Firefox","depth":4,"bounds":{"left":0.37632978,"top":0.15562649,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Safari","depth":4,"bounds":{"left":0.3849734,"top":0.15562649,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"32","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"81","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.009640957,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"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.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Crm\\Salesforce;\n\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Events\\Dispatcher;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Jiminny\\Component\\Country\\CountriesMap;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Enums\\CrmObject;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\NoResultsException;\nuse Jiminny\\Integrations\\PlaybookResolver;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\TeamSettings;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Salesforce\\Client;\nuse Jiminny\\Services\\Crm\\Salesforce\\PayloadBuilder;\nuse Jiminny\\Services\\Crm\\Salesforce\\QueryBuilder;\nuse Jiminny\\Services\\Crm\\Salesforce\\QueryHandler;\nuse Jiminny\\Services\\Crm\\Salesforce\\QueryIterator;\nuse Jiminny\\Services\\Crm\\Salesforce\\QueryResults;\nuse Jiminny\\Services\\Crm\\Salesforce\\Service;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\nuse Tests\\Unit\\Traits\\TestPrivateMethod;\n\nclass ServiceTest extends TestCase\n{\n use TestPrivateMethod;\n\n public function testFetchAndAssociateRelatedActivity(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $payloadBuilder->method('addCustomLogicFieldsPayload')\n ->willReturnCallback(function ($activity, $payload) {\n return $payload;\n });\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods(['fetchRelatedActivity', 'getPlaybook', 'getPlaybookCategory', 'updateRecord'])\n ->getMock();\n\n $serviceMock->expects($this->once())\n ->method('fetchRelatedActivity')\n ->willReturn([\n 'Id' => 'testId',\n 'Type' => null,\n 'OwnerId' => 'testerUser',\n 'Description' => 'Test description',\n ]);\n\n $user = $this->createMock(User::class);\n $team = $this->createMock(Team::class);\n $user->method('getAttribute')->with('team')->willReturn($team);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityField')->willReturn(null);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_EVENT);\n\n $serviceMock->expects($this->once())\n ->method('getPlaybook')\n ->with($user)\n ->willReturn($playbook);\n\n $serviceMock->expects($this->never())\n ->method('getPlaybookCategory');\n\n $serviceMock->expects($this->never())\n ->method('updateRecord');\n\n $fieldDataRepository = $this->createMock(FieldDataRepository::class);\n $fieldDataRepository->method('getActivityFieldData')->willReturn(collect([]));\n app()->instance(FieldDataRepository::class, $fieldDataRepository);\n\n $config = $this->createMock(Configuration::class);\n $profilesRelation = $this->getMockBuilder(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class)\n ->disableOriginalConstructor()\n ->onlyMethods(['get'])\n ->addMethods(['where'])\n ->getMock();\n $profilesRelation->method('where')->willReturnSelf();\n $profilesRelation->method('get')->willReturn(collect([]));\n $config->method('profiles')->willReturn($profilesRelation);\n\n $serviceMock->config = $config;\n $serviceMock->profile = null;\n\n $actualStartTime = \\Carbon\\Carbon::now();\n\n $activity = $this->getMockBuilder(Activity::class)\n ->disableOriginalConstructor()\n ->onlyMethods(['update', 'hasProspect'])\n ->getMock();\n\n $activity->method('update')->willReturn(true);\n $activity->method('hasProspect')->willReturn(true);\n\n $activity->type = Activity::TYPE_CONFERENCE;\n $activity->provider = Activity::PROVIDER_TWILIO;\n $activity->lead_id = 1;\n $activity->user_id = 0;\n $activity->id_string = 'test-activity-id';\n $activity->user = $user;\n\n $activity->actual_start_time = $actualStartTime;\n $activity->uuid = 'c53d8320-f556-4cee-a2f8-5f232f454ca4';\n\n app()->bind(PlaybookResolver::class, function () use ($user) {\n $playbook = $this->createMock(Playbook::class);\n $playbookResolver = $this->createMock(PlaybookResolver::class);\n $playbookResolver->expects($this->once())\n ->method('resolvePlaybookByUser')\n ->with($user)\n ->willReturn($playbook);\n\n return $playbookResolver;\n });\n\n $data = $serviceMock->fetchAndAssociateRelatedActivity($activity);\n\n $this->assertInstanceOf(Activity::class, $data);\n $this->assertEquals(Activity::TYPE_CONFERENCE, $data->getType());\n $this->assertEquals($actualStartTime->getTimestamp(), $data->getActualStartTime()->getTimestamp());\n }\n\n public function testFetchAndAssociateRelatedActivitySkipsForTaskBasedPlaybook(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods(['fetchRelatedActivity', 'getPlaybook'])\n ->getMock();\n\n $user = $this->createMock(User::class);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n $playbook->method('getId')->willReturn(123);\n\n $serviceMock->expects($this->once())\n ->method('getPlaybook')\n ->with($user)\n ->willReturn($playbook);\n\n $serviceMock->expects($this->never())\n ->method('fetchRelatedActivity');\n\n $activity = $this->getMockBuilder(Activity::class)\n ->disableOriginalConstructor()\n ->onlyMethods(['hasProspect', 'getUuid'])\n ->getMock();\n\n $activity->method('hasProspect')->willReturn(true);\n $activity->method('getUuid')->willReturn('c53d8320-f556-4cee-a2f8-5f232f454ca4');\n $activity->type = Activity::TYPE_CONFERENCE;\n $activity->actual_start_time = \\Carbon\\Carbon::now();\n $activity->user = $user;\n\n $result = $serviceMock->fetchAndAssociateRelatedActivity($activity);\n\n $this->assertNull($result);\n }\n\n public function testFetchAndAssociateRelatedActivityReturnsNullForNonConference(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $serviceMock = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class)\n );\n\n $activity = $this->getMockBuilder(Activity::class)\n ->disableOriginalConstructor()\n ->getMock();\n\n $activity->type = Activity::TYPE_SOFTPHONE;\n\n $result = $serviceMock->fetchAndAssociateRelatedActivity($activity);\n\n $this->assertNull($result);\n }\n\n public function testFetchAndAssociateRelatedActivityReturnsNullWhenNoStartTime(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $serviceMock = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class)\n );\n\n\n $activity = $this->getMockBuilder(Activity::class)\n ->disableOriginalConstructor()\n ->getMock();\n\n $activity->type = Activity::TYPE_CONFERENCE;\n $activity->actual_start_time = null;\n $activity->scheduled_start_time = null;\n\n $result = $serviceMock->fetchAndAssociateRelatedActivity($activity);\n\n $this->assertNull($result);\n }\n\n public function testFetchAndAssociateRelatedActivityReturnsNullWhenNoProspect(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods(['getPlaybook'])\n ->getMock();\n\n $serviceMock->expects($this->never())\n ->method('getPlaybook');\n\n $activity = $this->getMockBuilder(Activity::class)\n ->disableOriginalConstructor()\n ->onlyMethods(['hasProspect', 'getUuid'])\n ->getMock();\n\n $activity->method('hasProspect')->willReturn(false);\n $activity->method('getUuid')->willReturn('c53d8320-f556-4cee-a2f8-5f232f454ca4');\n $activity->type = Activity::TYPE_CONFERENCE;\n $activity->actual_start_time = \\Carbon\\Carbon::now();\n\n $result = $serviceMock->fetchAndAssociateRelatedActivity($activity);\n\n $this->assertNull($result);\n }\n\n public function testMatchExactlyByEmail(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods([])\n ->getMock();\n\n $profile = new Profile();\n $profile->setAttribute('id', bin2hex(random_bytes(8)));\n $serviceMock->profile = $profile;\n\n $team = $this->createMock(Team::class);\n $serviceMock->team = $team;\n\n $data = $serviceMock->matchExactlyByEmail(bin2hex(random_bytes(8)) . 'test_email@testserver.com');\n\n $this->assertEquals(null, $data);\n }\n\n public function testMatchDomainFromEmail(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $queryIterator = $this->createMock(QueryIterator::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $config = $this->createMock(Configuration::class);\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->any())\n ->method('search')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods(['convertCrmData'])\n ->getMock();\n\n $profile = new Profile();\n $profile->account_fields = 'Field1, Field2, Field3';\n $serviceMock->profile = $profile;\n\n $serviceMock->expects($this->once())\n ->method('convertCrmData')\n ->willReturn(['test']);\n\n $this->app->bind(QueryBuilder::class, function () {\n $queryBuilder = $this->createMock(QueryBuilder::class);\n $queryBuilder->expects($this->once())\n ->method('buildMatchByDomainQuery')\n ->with('test_email@testserver.com')\n ->willReturn('FIND {test_email@testserver.com} IN ALL FIELDS RETURNING Account(Id)');\n\n return $queryBuilder;\n });\n\n $team = $this->createMock(Team::class);\n\n $serviceMock->team = $team;\n $serviceMock->config = $config;\n\n $data = $serviceMock->matchByDomain('test_email@testserver.com');\n\n $this->assertEquals(['test'], $data);\n }\n\n public function testBuildTaskSearchFields(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class)\n );\n\n $fields = $service->buildTaskSearchFields();\n\n $expectedFields = ['Id', 'WhoId', 'WhatId', 'AccountId'];\n\n $this->assertEquals($expectedFields, $fields);\n }\n\n public function testMapCrmObjects(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class),\n );\n\n $sampleTask = [\n 'WhoId' => '003sampleWhoId',\n 'AccountId' => 'sampleAccountId',\n 'WhatId' => 'sampleWhatId',\n ];\n\n $activityData = $service->mapCrmObjects($sampleTask);\n\n $expectedActivityData = [\n 'contact' => '003sampleWhoId',\n 'account' => 'sampleAccountId',\n 'opportunity' => 'sampleWhatId',\n ];\n\n $this->assertEquals($expectedActivityData, $activityData);\n }\n\n public function testGetInstalledAppVersion(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n $queryIterator = $this->createMock(QueryIterator::class);\n $queryIterator->expects($this->any())\n ->method('current')->willReturn([\n 'SubscriberPackageVersion' => [\n 'MajorVersion' => '1',\n 'MinorVersion' => '0',\n 'PatchVersion' => '1',\n 'BuildNumber' => '0',\n ],\n ]);\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->any())\n ->method('metadata')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods(array_diff(get_class_methods(Service::class), ['getInstalledAppVersion']))\n ->getMock();\n\n $version = $serviceMock->getInstalledAppVersion();\n\n $this->assertEquals('1010', $version);\n }\n\n public function testSyncProfiles(): void\n {\n $userToSearch = null;\n\n $team = $this->createMock(Team::class);\n $config = $this->createMockedConfiguration();\n $config->expects($this->once())\n ->method('getId')\n ->willReturn(1);\n\n $salesforceUser = [\n 'Email' => 'test@example.com',\n 'UserPreferencesLightningExperiencePreferred' => true,\n 'CallCenterId' => '123',\n 'Id' => '456',\n 'ProfileId' => '789',\n ];\n\n app()->bind(QueryBuilder::class, function () use ($userToSearch) {\n $queryBuilder = $this->createMock(QueryBuilder::class);\n $queryBuilder->expects($this->once())\n ->method('buildGetUsersQuery')\n ->with($userToSearch)\n ->willReturn('SELECT * FROM Users');\n\n return $queryBuilder;\n });\n\n $queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults([$salesforceUser], 1, true, null));\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->any())\n ->method('query')\n ->with('SELECT * FROM Users')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n\n $user = $this->createMock(User::class);\n $user->expects($this->once())\n ->method('getId')\n ->willReturn(123);\n\n $this->mockTeamRepository($team, $salesforceUser, $user);\n\n $profileRepository = $this->createMock(ProfileRepository::class);\n $profileRepository->expects($this->once())\n ->method('updateOrCreateProfile')\n ->with(\n $user,\n [\n 'crm_configuration_id' => 1,\n 'crm_provider_id' => '456',\n ],\n [\n 'user_id' => 123,\n 'edition' => Profile::EDITION_LIGHTNING,\n 'has_external_cti' => true,\n 'crm_profile_id' => '789',\n ]\n )\n ->willReturn(new Profile());\n\n $this->app->instance(ProfileRepository::class, $profileRepository);\n\n $serviceMock = $this->getServiceMock();\n $serviceMock->team = $team;\n $serviceMock->config = $config;\n $result = $serviceMock->syncProfiles($userToSearch);\n\n $this->assertNull($result);\n }\n\n public function testSyncProfilesEmailIsNull(): void\n {\n $userToSearch = $this->createMock(User::class);\n\n $salesforceUser = [\n 'Email' => null,\n ];\n\n app()->bind(QueryBuilder::class, function () {\n $queryBuilder = $this->createMock(QueryBuilder::class);\n $queryBuilder->expects($this->once())\n ->method('buildGetUsersQuery')\n ->with(null)\n ->willReturn('SELECT * FROM Users');\n\n return $queryBuilder;\n });\n\n $queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults([$salesforceUser], 1, true, null));\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->exactly(2))\n ->method('query')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n\n $team = $this->createMock(Team::class);\n $user = $this->createMock(User::class);\n $this->mockTeamRepository($team, $salesforceUser, $user, false);\n\n $config = $this->createMock(Configuration::class);\n\n $serviceMock = $this->getServiceMock();\n $serviceMock->team = $team;\n $serviceMock->config = $config;\n\n $profile = $serviceMock->syncProfiles(null);\n\n $this->assertNull($profile);\n }\n\n public function testSyncProfilesUserToSearchMatchesCurrentUser(): void\n {\n $userToSearch = $this->createMock(User::class);\n $userToSearch->expects($this->once())\n ->method('getId')\n ->willReturn(123);\n\n $team = $this->createMock(Team::class);\n $config = $this->createMock(Configuration::class);\n $config->expects($this->once())\n ->method('getId')\n ->willReturn(1);\n\n $salesforceUser = [\n 'Email' => 'test@example.com',\n 'UserPreferencesLightningExperiencePreferred' => true,\n 'CallCenterId' => '123',\n 'Id' => '456',\n 'ProfileId' => '789',\n ];\n\n app()->bind(QueryBuilder::class, function () use ($userToSearch) {\n $queryBuilder = $this->createMock(QueryBuilder::class);\n $queryBuilder->expects($this->once())\n ->method('buildGetUsersQuery')\n ->with($userToSearch)\n ->willReturn('SELECT * FROM Users');\n\n return $queryBuilder;\n });\n\n $queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults([$salesforceUser], 1, true, null));\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->any())\n ->method('query')\n ->with('SELECT * FROM Users')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n\n $user = $this->createMock(User::class);\n $user->expects($this->exactly(2))\n ->method('getId')\n ->willReturn(123);\n\n $this->mockTeamRepository($team, $salesforceUser, $user);\n\n $profileRepository = $this->createMock(ProfileRepository::class);\n $profileRepository->expects($this->once())\n ->method('updateOrCreateProfile')\n ->with(\n $user,\n [\n 'crm_configuration_id' => 1,\n 'crm_provider_id' => '456',\n ],\n [\n 'user_id' => 123,\n 'edition' => Profile::EDITION_LIGHTNING,\n 'has_external_cti' => true,\n 'crm_profile_id' => '789',\n ]\n )\n ->willReturn(new Profile());\n\n $this->app->instance(ProfileRepository::class, $profileRepository);\n\n $serviceMock = $this->getServiceMock();\n $serviceMock->team = $team;\n $serviceMock->config = $config;\n $profile = $serviceMock->syncProfiles($userToSearch);\n\n $this->assertInstanceOf(Profile::class, $profile);\n }\n\n public function testSyncProfilesWithCustomValidation(): void\n {\n $userToSearch = null;\n\n $team = $this->createMock(Team::class);\n $config = $this->createMockedConfiguration();\n $config->expects($this->atLeastOnce()) // Changed from once() to atLeastOnce()\n ->method('getId')\n ->willReturn(1);\n\n $salesforceUser = [\n 'Email' => 'test@example.com',\n 'UserPreferencesLightningExperiencePreferred' => true,\n 'CallCenterId' => '123',\n 'Id' => '456',\n 'ProfileId' => '789',\n 'CustomField' => 'CustomValue',\n ];\n\n $customRules = [\n ['field' => 'CustomField', 'value' => 'CustomValue'],\n ];\n\n $this->mockQueryBuilderAndHandler($userToSearch, [$salesforceUser]);\n\n $user = $this->createMock(User::class);\n $user->expects($this->once())\n ->method('getId')\n ->willReturn(123);\n\n $this->mockTeamRepository($team, $salesforceUser, $user, true, $customRules);\n\n $this->mockProfileRepository($user);\n\n $serviceMock = $this->getServiceMock();\n $serviceMock->team = $team;\n $serviceMock->config = $config;\n $profile = $serviceMock->syncProfiles($userToSearch);\n\n $this->assertNull($profile);\n }\n\n public function testSyncProfilesWithCustomValidationFailing(): void\n {\n $userToSearch = null;\n\n $team = $this->createMock(Team::class);\n $config = $this->createMockedConfiguration();\n\n $salesforceUser = [\n 'Email' => 'test@example.com',\n 'UserPreferencesLightningExperiencePreferred' => true,\n 'CallCenterId' => '123',\n 'Id' => '456',\n 'ProfileId' => '789',\n 'CustomField' => 'WrongValue',\n ];\n\n $customRules = [\n ['field' => 'CustomField', 'value' => 'CustomValue'],\n ];\n\n $this->mockQueryBuilderAndHandler($userToSearch, [$salesforceUser]);\n\n $teamRepository = $this->getMockForAbstractClass(TeamRepository::class, [], '', false, true, true, ['findActiveTeamMemberByEmail', 'getTeamSetting']);\n\n $teamSettings = $this->createMock(TeamSettings::class);\n $teamSettings->method('getValueType')\n ->willReturn('array');\n\n $teamSettings->method('getValue')\n ->willReturn(json_encode($customRules));\n\n $teamRepository->expects($this->once())\n ->method('getTeamSetting')\n ->with($team, 'custom_profile_validation')\n ->willReturn($teamSettings);\n\n app()->bind(TeamRepository::class, function () use ($teamRepository) {\n return $teamRepository;\n });\n\n $profileRepository = $this->createMock(ProfileRepository::class);\n $profileRepository->expects($this->never())\n ->method('updateOrCreateProfile');\n\n $this->app->instance(ProfileRepository::class, $profileRepository);\n\n $serviceMock = $this->getServiceMock();\n $serviceMock->team = $team;\n $serviceMock->config = $config;\n $result = $serviceMock->syncProfiles($userToSearch);\n\n $this->assertNull($result);\n }\n\n public function testGetContactRolesFromCrm(): void\n {\n $contactRoles = [\n [\n 'Id' => '1',\n 'ContactId' => 'Contact1',\n 'OpportunityId' => 'Opportunity1',\n 'Opportunity' => ['OwnerId' => 'Owner1'],\n 'IsPrimary' => true,\n 'Role' => 'Decision Maker',\n ],\n ];\n\n $expectedResponse = [\n [\n 'id' => '1',\n 'contactId' => 'Contact1',\n 'opportunityId' => 'Opportunity1',\n 'ownerId' => 'Owner1',\n 'isPrimary' => true,\n 'role' => 'Decision Maker',\n ],\n ];\n\n $this->bindQueryIterator($contactRoles);\n\n $serviceMock = $this->getServiceMock();\n\n $data = $serviceMock->getContactRolesFromCrm(now()->subDay());\n\n $this->assertEquals($expectedResponse, $data);\n }\n\n public function testGetContactRolesFromCrmNoResult(): void\n {\n $this->bindQueryIterator([]);\n\n $serviceMock = $this->getServiceMock();\n\n $data = $serviceMock->getContactRolesFromCrm(now()->subDay());\n\n $this->assertEquals([], $data);\n }\n\n public function testSyncContactRoles(): void\n {\n $contactRoles = [\n [\n 'id' => '1',\n 'contactId' => 'Contact1',\n 'opportunityId' => 'Opportunity1',\n 'ownerId' => 'Owner1',\n 'isPrimary' => true,\n 'role' => 'Decision Maker',\n ],\n ];\n\n app()->bind(ContactRoleRepository::class, function () {\n $contactRoleRepository = $this->createMock(ContactRoleRepository::class);\n $contactRoleRepository->expects($this->once())\n ->method('saveContactRoles');\n\n return $contactRoleRepository;\n });\n\n $serviceMock = $this->getServiceMock([\n 'getContactRolesFromCrm',\n 'syncRemotelyDeletedContactRoles',\n 'syncContact',\n 'syncOpportunity',\n ]);\n\n $config = $this->createMock(Configuration::class);\n $hasMany = $this->createMock(HasManyExtended::class);\n $hasMany->expects($this->exactly(2))\n ->method('where')\n ->willReturn($hasMany);\n\n $hasMany->expects($this->exactly(2))\n ->method('first')\n ->willReturn(\n $this->createMock(Contact::class),\n $this->createMock(Opportunity::class)\n );\n\n $config->expects($this->once())\n ->method('contacts')\n ->willReturn($hasMany);\n $config->expects($this->once())\n ->method('opportunities')\n ->willReturn($hasMany);\n\n $serviceMock->config = $config;\n\n $serviceMock->expects($this->once())\n ->method('getContactRolesFromCrm')\n ->willReturn($contactRoles);\n\n $serviceMock->expects($this->once())\n ->method('syncRemotelyDeletedContactRoles');\n\n $data = $serviceMock->syncContactRoles(now()->subDay());\n\n $this->assertEquals(1, $data);\n }\n\n public function testSyncRemotelyDeletedContactRoles(): void\n {\n $contactRoles = [\n [\n 'id' => '1',\n 'crm_provider_id' => '1',\n ],\n ];\n\n app()->bind(QueryHandler::class, function () use ($contactRoles) {\n $queryResults = new QueryResults($contactRoles, 1, true, null);\n\n $handler = $this->createMock(QueryHandler::class);\n $handler->method('queryDeleted')\n ->willReturn($queryResults);\n\n return $handler;\n });\n\n app()->bind(ContactRoleRepository::class, function () {\n $contactRoleRepository = $this->createMock(ContactRoleRepository::class);\n $contactRoleRepository->expects($this->once())\n ->method('deleteContactRoles');\n\n return $contactRoleRepository;\n });\n\n $serviceMock = $this->getServiceMock();\n $serviceMock->team = $this->createMock(Team::class);\n\n $data = $this->invokePrivateMethod('syncRemotelyDeletedContactRoles', $serviceMock, []);\n\n $this->assertTrue($data);\n }\n\n private function bindQueryIterator(array $queryResult): void\n {\n /** @var Client $client */\n $client = $this->createMock(Client::class);\n $queryIterator = new QueryIterator(\n $client,\n new QueryResults($queryResult, 1, true, null)\n );\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->any())\n ->method('query')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n }\n\n public static function getOpportunitySortOrderDataProvider(): array\n {\n return [\n 'all open recently updated' => [Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED, ['LastModifiedDate DESC', true]],\n 'all open recently created' => [Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED, ['CreatedDate DESC', true]],\n 'all open oldest created' => [Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED, ['CreatedDate ASC', true]],\n 'all recently updated' => [Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED, ['LastModifiedDate DESC', false]],\n 'default' => ['unknown', ['LastModifiedDate DESC', true]],\n ];\n }\n\n private function createMockedConfiguration(): Configuration\n {\n $config = $this->createMock(Configuration::class);\n $profilesRelation = $this->getMockBuilder(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class)\n ->disableOriginalConstructor()\n ->onlyMethods(['get'])\n ->addMethods(['where', 'first'])\n ->getMock();\n $profilesRelation->method('where')->willReturnSelf();\n $profilesRelation->method('get')->willReturn(collect([]));\n $profilesRelation->method('first')->willReturn(null);\n $config->method('profiles')->willReturn($profilesRelation);\n\n return $config;\n }\n\n private function getServiceMock(array $onlyMethods = []): MockObject&Service\n {\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $this->createMock(Client::class),\n $this->createMock(PayloadBuilder::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(CountriesMap::class),\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods($onlyMethods)\n ->getMock();\n\n $serviceMock->profile = $this->createMock(Profile::class);\n\n return $serviceMock;\n }\n\n private function mockQueryBuilderAndHandler($userToSearch, $salesforceUsers): void\n {\n app()->bind(QueryBuilder::class, function () use ($userToSearch) {\n $queryBuilder = $this->createMock(QueryBuilder::class);\n $queryBuilder->expects($this->once())\n ->method('buildGetUsersQuery')\n ->with($userToSearch)\n ->willReturn('SELECT * FROM Users');\n\n return $queryBuilder;\n });\n\n $queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults($salesforceUsers, count($salesforceUsers), true, null));\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->any())\n ->method('query')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n }\n\n private function mockTeamRepository(\n Team $team,\n array $salesforceUser,\n ?User $user = null,\n bool $userSearch = true,\n array $customRules = []\n ): void {\n $teamRepository = $this->getMockForAbstractClass(TeamRepository::class, [], '', false, true, true, ['findActiveTeamMemberByEmail', 'getTeamSetting']);\n\n if ($userSearch) {\n $teamRepository->expects($this->once())\n ->method('findActiveTeamMemberByEmail')\n ->with($team, $salesforceUser['Email'])\n ->willReturn($user);\n }\n\n $teamSettings = $this->createMock(TeamSettings::class);\n $teamSettings->method('getValueType')\n ->willReturn('array');\n\n $teamSettings->method('getValue')\n ->willReturn(json_encode($customRules));\n\n $teamRepository->expects($this->once())\n ->method('getTeamSetting')\n ->with($team, 'custom_profile_validation')\n ->willReturn($teamSettings);\n\n app()->bind(TeamRepository::class, function () use ($teamRepository) {\n return $teamRepository;\n });\n }\n\n private function mockProfileRepository(User $user): void\n {\n $profileRepository = $this->createMock(ProfileRepository::class);\n $profileRepository->expects($this->once())\n ->method('updateOrCreateProfile')\n ->with(\n $user,\n [\n 'crm_configuration_id' => 1,\n 'crm_provider_id' => '456',\n ],\n [\n 'user_id' => 123,\n 'edition' => Profile::EDITION_LIGHTNING,\n 'has_external_cti' => true,\n 'crm_profile_id' => '789',\n ]\n )\n ->willReturn(new Profile());\n\n $this->app->instance(ProfileRepository::class, $profileRepository);\n }\n\n public function testBuildEnhancedNoteDecodesHtmlEntities(): void\n {\n $service = $this->getServiceMock(['createRecord']);\n\n $profile = new Profile();\n $profile->setAttribute('crm_provider_id', 'owner-123');\n $service->profile = $profile;\n\n $service->expects($this->exactly(2))\n ->method('createRecord')\n ->willReturnOnConsecutiveCalls('note-id-123', 'link-id-456');\n\n $bodyWithEntities = 'Welch's current challenges and Facebook's Club';\n\n $result = $this->invokePrivateMethod('buildEnhancedNote', $service, [\n 'Test Title',\n $bodyWithEntities,\n 'object-id-789',\n ]);\n\n $this->assertEquals('note-id-123', $result);\n }\n\n public function testBuildEnhancedNoteSanitizesWithoutQuotes(): void\n {\n $service = $this->getServiceMock(['createRecord']);\n\n $profile = new Profile();\n $profile->setAttribute('crm_provider_id', 'owner-456');\n $service->profile = $profile;\n\n $service->expects($this->exactly(2))\n ->method('createRecord')\n ->willReturnCallback(function ($type, $data) {\n if ($type === 'ContentNote') {\n $decoded = base64_decode($data['Content']);\n $this->assertStringContainsString(\"Welch's\", $decoded);\n $this->assertStringNotContainsString(''', $decoded);\n $this->assertStringNotContainsString('&#039;', $decoded);\n $this->assertStringContainsString('<script>', $decoded);\n\n return 'note-id-456';\n }\n\n return 'link-id-789';\n });\n\n $bodyWithMixedContent = \"Welch's and <script>alert('xss')</script>\";\n\n $result = $this->invokePrivateMethod('buildEnhancedNote', $service, [\n 'Test Title',\n $bodyWithMixedContent,\n 'object-id-123',\n ]);\n\n $this->assertEquals('note-id-456', $result);\n }\n\n public function testBuildEnhancedNoteConvertsLineBreaks(): void\n {\n $service = $this->getServiceMock(['createRecord']);\n\n $profile = new Profile();\n $profile->setAttribute('crm_provider_id', 'owner-789');\n $service->profile = $profile;\n\n $service->expects($this->exactly(2))\n ->method('createRecord')\n ->willReturnCallback(function ($type, $data) {\n if ($type === 'ContentNote') {\n $decoded = base64_decode($data['Content']);\n $this->assertStringContainsString('<br>', $decoded);\n $this->assertStringNotContainsString('<br />', $decoded);\n\n return 'note-id-789';\n }\n\n return 'link-id-012';\n });\n\n $bodyWithLineBreaks = \"Line 1\\nLine 2\\nLine 3\";\n\n $result = $this->invokePrivateMethod('buildEnhancedNote', $service, [\n 'Test Title',\n $bodyWithLineBreaks,\n 'object-id-456',\n ]);\n\n $this->assertEquals('note-id-789', $result);\n }\n\n public function testBuildEnhancedNoteHandlesComplexScenario(): void\n {\n $service = $this->getServiceMock(['createRecord']);\n\n $profile = new Profile();\n $profile->setAttribute('crm_provider_id', 'owner-complex');\n $service->profile = $profile;\n\n $service->expects($this->exactly(2))\n ->method('createRecord')\n ->willReturnCallback(function ($type, $data) {\n if ($type === 'ContentNote') {\n $decoded = base64_decode($data['Content']);\n\n $this->assertStringContainsString(\"Welch's\", $decoded);\n $this->assertStringContainsString(\"Facebook's Club\", $decoded);\n $this->assertStringContainsString(\"Arctics'\", $decoded);\n $this->assertStringNotContainsString(''', $decoded);\n $this->assertStringNotContainsString('&#039;', $decoded);\n $this->assertStringContainsString('<br>', $decoded);\n $this->assertStringContainsString('<', $decoded);\n $this->assertStringContainsString('>', $decoded);\n\n return 'note-complex';\n }\n\n return 'link-complex';\n });\n\n $complexBody = \"Summary:\\n---------\\nThe call focused on understanding Welch's current challenges and exploring how Arctics' Revenue Growth Management solutions could support their strategic goals.\\n\\n• John SMith discussed his role as a category advisor for Google and Facebook's Club, emphasizing the importance of market research and advising on product assortment.\\n• Madona introduced Arctics' Virtual Shoppers AI, which simulates consumer <behavior> to optimize pricing and promotional strategies.\";\n\n $result = $this->invokePrivateMethod('buildEnhancedNote', $service, [\n 'Jiminny Transcription Summary',\n $complexBody,\n 'task-id-001',\n ]);\n\n $this->assertEquals('note-complex', $result);\n }\n\n public function testSyncRemotelyDeletedObjectsWithErrorHandlingSuccess(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['syncRemotelyDeletedObjects'])\n ->getMock();\n\n // Mock team\n $team = $this->createMock(Team::class);\n $team->method('getUuid')->willReturn('team-uuid-123');\n $serviceMock->team = $team;\n\n // Expect syncRemotelyDeletedObjects to be called once and succeed\n $serviceMock->expects($this->once())\n ->method('syncRemotelyDeletedObjects')\n ->with(\\Jiminny\\Enums\\CrmObject::ACCOUNT);\n\n // Call the protected method using reflection\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');\n $method->setAccessible(true);\n\n // Should not throw any exceptions\n $method->invoke($serviceMock, \\Jiminny\\Enums\\CrmObject::ACCOUNT);\n\n $this->assertTrue(true); // Test completed successfully\n }\n\n public function testSyncRemotelyDeletedObjectsWithErrorHandlingFailure(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['syncRemotelyDeletedObjects'])\n ->getMock();\n\n // Mock team\n $team = $this->createMock(Team::class);\n $team->method('getUuid')->willReturn('team-uuid-456');\n $serviceMock->team = $team;\n\n // Mock logger to verify warning is logged\n $logger = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n\n // Use reflection to set the protected logger property\n $reflection = new \\ReflectionClass($serviceMock);\n $loggerProperty = $reflection->getProperty('logger');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($serviceMock, $logger);\n\n $exception = new \\Exception('Sync failed due to API error');\n\n // Expect syncRemotelyDeletedObjects to throw an exception\n $serviceMock->expects($this->once())\n ->method('syncRemotelyDeletedObjects')\n ->with(\\Jiminny\\Enums\\CrmObject::CONTACT)\n ->willThrowException($exception);\n\n // Expect warning to be logged with correct message and parameters\n $logger->expects($this->once())\n ->method('warning')\n ->with(\n '[Salesforce] Remotely deleted objects sync failed',\n [\n 'objectType' => 'contact',\n 'teamId' => 'team-uuid-456',\n 'reason' => 'Sync failed due to API error',\n ]\n );\n\n // Call the protected method using reflection\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');\n $method->setAccessible(true);\n\n // Should not re-throw the exception, just log it\n $method->invoke($serviceMock, \\Jiminny\\Enums\\CrmObject::CONTACT);\n\n $this->assertTrue(true); // Test completed successfully\n }\n\n public function testSyncRemotelyDeletedObjectsWithErrorHandlingWithLogParams(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['syncRemotelyDeletedObjects'])\n ->getMock();\n\n // Mock team\n $team = $this->createMock(Team::class);\n $team->method('getUuid')->willReturn('team-uuid-789');\n $serviceMock->team = $team;\n\n // Mock logger to verify warning is logged\n $logger = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n\n // Use reflection to set the protected logger property\n $loggerReflection = new \\ReflectionClass($serviceMock);\n $loggerProperty = $loggerReflection->getProperty('logger');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($serviceMock, $logger);\n\n $exception = new \\Exception('Network timeout');\n\n // Expect syncRemotelyDeletedObjects to throw an exception\n $serviceMock->expects($this->once())\n ->method('syncRemotelyDeletedObjects')\n ->with(\\Jiminny\\Enums\\CrmObject::OPPORTUNITY)\n ->willThrowException($exception);\n\n // Additional log parameters\n $logParams = [\n 'syncType' => 'full',\n 'batchSize' => 100,\n ];\n\n // Expect warning to be logged with merged parameters\n $logger->expects($this->once())\n ->method('warning')\n ->with(\n '[Salesforce] Remotely deleted objects sync failed',\n [\n 'objectType' => 'opportunity',\n 'teamId' => 'team-uuid-789',\n 'reason' => 'Network timeout',\n 'syncType' => 'full',\n 'batchSize' => 100,\n ]\n );\n\n // Call the protected method using reflection\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');\n $method->setAccessible(true);\n\n // Should not re-throw the exception, just log it\n $method->invoke($serviceMock, \\Jiminny\\Enums\\CrmObject::OPPORTUNITY, $logParams);\n\n $this->assertTrue(true); // Test completed successfully\n }\n\n /**\n * @dataProvider crmObjectProvider\n */\n public function testSyncRemotelyDeletedObjectsWithErrorHandlingDifferentCrmObjects(\\Jiminny\\Enums\\CrmObject $crmObject): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['syncRemotelyDeletedObjects'])\n ->getMock();\n\n // Mock team\n $team = $this->createMock(Team::class);\n $team->method('getUuid')->willReturn('team-uuid-test');\n $serviceMock->team = $team;\n\n // Mock logger to verify warning is logged\n $logger = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n\n // Use reflection to set the protected logger property\n $loggerReflectionClass = new \\ReflectionClass($serviceMock);\n $loggerProperty = $loggerReflectionClass->getProperty('logger');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($serviceMock, $logger);\n\n $exception = new \\Exception('Test error');\n\n // Expect syncRemotelyDeletedObjects to throw an exception\n $serviceMock->expects($this->once())\n ->method('syncRemotelyDeletedObjects')\n ->with($crmObject)\n ->willThrowException($exception);\n\n // Expect warning to be logged with correct entity type\n $logger->expects($this->once())\n ->method('warning')\n ->with(\n '[Salesforce] Remotely deleted objects sync failed',\n [\n 'objectType' => $crmObject->value,\n 'teamId' => 'team-uuid-test',\n 'reason' => 'Test error',\n ]\n );\n\n // Call the protected method using reflection\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');\n $method->setAccessible(true);\n\n $method->invoke($serviceMock, $crmObject);\n\n $this->assertTrue(true); // Test completed successfully\n }\n\n public function testHandleObjectDeletionWithDeletedEntity(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['deleteCrmObject'])\n ->getMock();\n\n $entity = $this->createMock(\\Jiminny\\Models\\Account::class);\n $crmData = ['IsDeleted' => true];\n\n $serviceMock->expects($this->once())\n ->method('deleteCrmObject')\n ->with($entity);\n\n // Use reflection to call the protected method\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('handleObjectDeletion');\n $method->setAccessible(true);\n\n $method->invoke($serviceMock, $entity, $crmData);\n }\n\n public function testHandleObjectDeletionWithNonDeletedEntity(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['deleteCrmObject'])\n ->getMock();\n\n $entity = $this->createMock(\\Jiminny\\Models\\Contact::class);\n $crmData = ['IsDeleted' => false];\n\n $serviceMock->expects($this->never())\n ->method('deleteCrmObject');\n\n // Use reflection to call the protected method\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('handleObjectDeletion');\n $method->setAccessible(true);\n\n $method->invoke($serviceMock, $entity, $crmData);\n }\n\n public function testDeleteCrmObjectWithValidEntity(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['dispatchDeleteCrmObjectJob'])\n ->getMock();\n\n $entity = $this->createMock(\\Jiminny\\Models\\Lead::class);\n $entity->expects($this->once())->method('delete');\n\n $serviceMock->expects($this->once())\n ->method('dispatchDeleteCrmObjectJob')\n ->with($entity);\n\n // Use reflection to call the protected method\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('deleteCrmObject');\n $method->setAccessible(true);\n\n $method->invoke($serviceMock, $entity);\n }\n\n public function testDeleteCrmObjectWithNullEntity(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['dispatchDeleteCrmObjectJob'])\n ->getMock();\n\n $serviceMock->expects($this->never())\n ->method('dispatchDeleteCrmObjectJob');\n\n // Use reflection to call the protected method\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('deleteCrmObject');\n $method->setAccessible(true);\n\n $method->invoke($serviceMock, null);\n }\n\n public function testDispatchDeleteCrmObjectJobWithNullEntity(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $prospectPhotoPathService,\n );\n\n // Use reflection to call the protected method\n $reflection = new \\ReflectionClass($service);\n $method = $reflection->getMethod('dispatchDeleteCrmObjectJob');\n $method->setAccessible(true);\n\n // Should return early without dispatching - no exception expected\n $method->invoke($service, null);\n\n $this->assertTrue(true); // Test completed successfully\n }\n\n public function testDispatchDeleteCrmObjectJobWithUnsupportedEntity(): void\n {\n $this->expectException(\\TypeError::class);\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $prospectPhotoPathService,\n );\n\n $unsupportedEntity = $this->createMock(\\stdClass::class);\n\n // Use reflection to call the protected method\n $reflection = new \\ReflectionClass($service);\n $method = $reflection->getMethod('dispatchDeleteCrmObjectJob');\n\n // This will throw TypeError due to union type constraint\n $method->invoke($service, $unsupportedEntity);\n }\n\n public function testHandleEntityDeletionByProviderIdMethodExists(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $prospectPhotoPathService,\n );\n\n // Test that the method exists and is accessible via reflection\n $reflection = new \\ReflectionClass($service);\n $method = $reflection->getMethod('handleEntityDeletionByProviderId');\n $method->setAccessible(true);\n\n // Verify method exists and has correct parameters\n $this->assertTrue($method->isProtected());\n $this->assertEquals(2, $method->getNumberOfParameters());\n\n $parameters = $method->getParameters();\n $this->assertEquals('targetEntity', $parameters[0]->getName());\n $this->assertEquals('crmData', $parameters[1]->getName());\n }\n\n public function testSyncRemotelyDeletedObjectsWithNoResults(): void\n {\n // Create a real service instance to avoid mock property issues\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $prospectPhotoPathService,\n );\n\n // Mock queryHandler to throw NoResultsException\n $queryHandler = $this->createMock(QueryHandler::class);\n $queryHandler->expects($this->once())\n ->method('queryDeleted')\n ->with('Opportunity')\n ->willThrowException(new NoResultsException('No results'));\n\n // Set the queryHandler using reflection on the real service\n $reflection = new \\ReflectionClass($service);\n $queryHandlerProperty = $reflection->getProperty('queryHandler');\n $queryHandlerProperty->setAccessible(true);\n $queryHandlerProperty->setValue($service, $queryHandler);\n\n $result = self::invokePrivateMethod('syncRemotelyDeletedObjects', $service, [CrmObject::OPPORTUNITY]);\n\n $this->assertFalse($result);\n }\n\n public function testSyncRemotelyDeletedObjectsWithEmptyResults(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $prospectPhotoPathService,\n );\n\n // Mock queryHandler to return empty results\n $queryResult = $this->createMock(QueryResults::class);\n $queryResult->method('getResults')->willReturn([]);\n\n $queryHandler = $this->createMock(QueryHandler::class);\n $queryHandler->expects($this->once())\n ->method('queryDeleted')\n ->with('Opportunity')\n ->willReturn($queryResult);\n\n // Set the queryHandler using reflection on the real service\n $reflection = new \\ReflectionClass($service);\n $queryHandlerProperty = $reflection->getProperty('queryHandler');\n $queryHandlerProperty->setAccessible(true);\n $queryHandlerProperty->setValue($service, $queryHandler);\n\n $result = self::invokePrivateMethod('syncRemotelyDeletedObjects', $service, [CrmObject::OPPORTUNITY]);\n\n $this->assertFalse($result);\n }\n\n public function testSyncRemotelyDeletedObjectsWithUnsupportedCrmObject(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $prospectPhotoPathService,\n );\n\n // Mock queryHandler to return some deleted objects so we reach the match statement\n $deletedObjects = [\n ['id' => 'task1'],\n ['id' => 'task2'],\n ];\n $queryResult = $this->createMock(QueryResults::class);\n $queryResult->method('getResults')->willReturn($deletedObjects);\n\n $queryHandler = $this->createMock(QueryHandler::class);\n $queryHandler->expects($this->once())\n ->method('queryDeleted')\n ->with('Task') // ucfirst('task') = 'Task'\n ->willReturn($queryResult);\n\n self::setPrivateProperty($service, 'queryHandler', $queryHandler);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Unsupported CrmObject: task');\n\n self::invokePrivateMethod('syncRemotelyDeletedObjects', $service, [CrmObject::TASK]);\n }\n\n public static function crmObjectProvider(): array\n {\n return [\n 'Account' => [CrmObject::ACCOUNT],\n 'Contact' => [CrmObject::CONTACT],\n 'Lead' => [CrmObject::LEAD],\n 'Opportunity' => [CrmObject::OPPORTUNITY],\n ];\n }\n\n public function testVerifyTaskExistsReturnsTrue(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:task-123', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-123');\n $activity->method('getId')->willReturn(456);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Task', 'task-123', ['Id', 'IsDeleted'])\n ->willReturn(['Id' => 'task-123', 'IsDeleted' => false]);\n\n $result = $service->verifyTaskExists($activity);\n\n $this->assertTrue($result);\n }\n\n public function testVerifyTaskExistsReturnsTrueForEvent(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:event-123', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('event-123');\n $activity->method('getId')->willReturn(456);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_EVENT);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Event', 'event-123', ['Id', 'IsDeleted'])\n ->willReturn(['Id' => 'event-123', 'IsDeleted' => false]);\n\n $result = $service->verifyTaskExists($activity);\n\n $this->assertTrue($result);\n }\n\n public function testVerifyTaskExistsReturnsFalseWhenDeleted(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:task-456', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-456');\n $activity->method('getId')->willReturn(789);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Task', 'task-456', ['Id', 'IsDeleted'])\n ->willReturn(['Id' => 'task-456', 'IsDeleted' => true]);\n\n $result = $service->verifyTaskExists($activity);\n\n $this->assertFalse($result);\n }\n\n public function testVerifyTaskExistsReturnsFalseWhenNotFound(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:task-999', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-999');\n $activity->method('getId')->willReturn(999);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Task', 'task-999', ['Id', 'IsDeleted'])\n ->willThrowException(new \\Jiminny\\Exceptions\\HttpNotFoundException('Task not found'));\n\n $result = $service->verifyTaskExists($activity);\n\n $this->assertFalse($result);\n }\n\n public function testVerifyTaskExistsReturnsFalseWhenNoPlaybook(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:task-no-playbook', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-no-playbook');\n $activity->method('getId')->willReturn(111);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn(null);\n\n $result = $service->verifyTaskExists($activity);\n\n $this->assertFalse($result);\n }\n\n public function testVerifyTaskExistsThrowsExceptionForTransientErrors(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:task-error', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-error');\n $activity->method('getId')->willReturn(888);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Task', 'task-error', ['Id', 'IsDeleted'])\n ->willThrowException(new \\RuntimeException('Network timeout'));\n\n $this->expectException(\\RuntimeException::class);\n $this->expectExceptionMessage('Network timeout');\n\n $service->verifyTaskExists($activity);\n }\n\n public function testVerifyTaskExistsCachesResults(): void\n {\n $cachedValue = null;\n Cache::shouldReceive('remember')\n ->twice()\n ->with('crm_task_exists:123:task-cached', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(function ($key, $ttl, $callback) use (&$cachedValue) {\n if ($cachedValue === null) {\n $cachedValue = $callback();\n }\n\n return $cachedValue;\n });\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-cached');\n $activity->method('getId')->willReturn(555);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Task', 'task-cached', ['Id', 'IsDeleted'])\n ->willReturn(['Id' => 'task-cached', 'IsDeleted' => false]);\n\n $result1 = $service->verifyTaskExists($activity);\n $result2 = $service->verifyTaskExists($activity);\n\n $this->assertTrue($result1);\n $this->assertTrue($result2);\n }\n\n public function testVerifyTaskExistsReturnsFalseForHttpBadRequestException(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:task-400', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-400');\n $activity->method('getId')->willReturn(400);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Task', 'task-400', ['Id', 'IsDeleted'])\n ->willThrowException(new \\Jiminny\\Exceptions\\HttpBadRequestException('Bad request'));\n\n $result = $service->verifyTaskExists($activity);\n\n $this->assertFalse($result);\n }\n\n public function testImportOpportunitySkipsWhenNoProfileAndNoAccount(): void\n {\n $crmData = [\n 'Id' => 'SF-NO-USER-1',\n 'Name' => 'Test Opportunity',\n 'OwnerId' => 'owner-no-profile',\n // No AccountId\n ];\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(\\Illuminate\\Events\\Dispatcher::class); // ← ADD THIS\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n\n $service = new Service(\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService\n );\n\n $config = $this->createMock(Configuration::class);\n\n // Mock profiles relation returning null (no profile found)\n $profilesRelation = \\Mockery::mock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $profilesRelation->shouldReceive('where')->with('crm_provider_id', 'owner-no-profile')->andReturnSelf();\n $profilesRelation->shouldReceive('first')->andReturn(null);\n\n $config->expects($this->once())\n ->method('profiles')\n ->willReturn($profilesRelation);\n\n $team = $this->createMock(Team::class);\n $team->method('getId')->willReturn(1);\n\n $logger = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $logger->expects($this->once())\n ->method('error')\n ->with(\n '[Salesforce] | Skip import, no user_id found',\n ['id' => 'SF-NO-USER-1']\n );\n\n $reflection = new \\ReflectionClass($service);\n\n $configProperty = $reflection->getProperty('config');\n $configProperty->setAccessible(true);\n $configProperty->setValue($service, $config);\n\n $teamProperty = $reflection->getProperty('team');\n $teamProperty->setAccessible(true);\n $teamProperty->setValue($service, $team);\n\n $loggerProperty = $reflection->getProperty('logger');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($service, $logger);\n\n // Initialize profile property to avoid \"must not be accessed before initialization\" error\n $profileProperty = $reflection->getProperty('profile');\n $profileProperty->setAccessible(true);\n $profileProperty->setValue($service, null);\n\n $result = self::invokePrivateMethod('importOpportunity', $service, [$crmData]);\n\n $this->assertNull($result);\n }\n\n public function testImportContactReturnsNullWhenIsDeleted(): void\n {\n $crmData = ['Id' => 'SF-CON-DEL', 'IsDeleted' => true];\n\n $contactsRelation = $this->getMockBuilder(HasMany::class)\n ->disableOriginalConstructor()\n ->addMethods(['where', 'first'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->expects($this->once())->method('contacts')->willReturn($contactsRelation);\n\n $service = $this->getServiceMock(['handleEntityDeletionByProviderId']);\n $service->config = $config;\n\n $service->expects($this->once())\n ->method('handleEntityDeletionByProviderId')\n ->with($contactsRelation, $crmData);\n\n $result = self::invokePrivateMethod('importContact', $service, [$crmData]);\n\n $this->assertNull($result);\n }\n\n public function testImportContactSkipsWritesWhenIsDeleted(): void\n {\n $crmData = ['Id' => 'SF-CON-DEL-2', 'IsDeleted' => true];\n\n $contactsRelation = $this->getMockBuilder(HasMany::class)\n ->disableOriginalConstructor()\n ->addMethods(['where', 'first'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->expects($this->once())->method('contacts')->willReturn($contactsRelation);\n\n $service = $this->getServiceMock(['handleEntityDeletionByProviderId']);\n $service->config = $config;\n\n $service->expects($this->once())->method('handleEntityDeletionByProviderId');\n\n $result = self::invokePrivateMethod('importContact', $service, [$crmData]);\n\n $this->assertNull($result);\n }\n\n public function testImportContactReturnsTrashedContactAsNull(): void\n {\n $crmData = [\n 'Id' => 'SF-CON-TRASHED',\n 'IsDeleted' => false,\n 'OwnerId' => null,\n 'Name' => 'Trashed Contact',\n ];\n\n $contact = $this->createMock(Contact::class);\n $contact->method('trashed')->willReturn(true);\n\n $contactsRelation = $this->getMockBuilder(HasMany::class)\n ->disableOriginalConstructor()\n ->addMethods(['where', 'first', 'withTrashed'])\n ->onlyMethods(['updateOrCreate'])\n ->getMock();\n $contactsRelation->method('where')->willReturnSelf();\n $contactsRelation->method('withTrashed')->willReturnSelf();\n $contactsRelation->method('first')->willReturn(null);\n $contactsRelation->method('updateOrCreate')->willReturn($contact);\n\n $config = $this->createMock(Configuration::class);\n $config->method('contacts')->willReturn($contactsRelation);\n $config->method('accounts')->willReturn($contactsRelation);\n\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $prospectPhotoPathService->method('getOrGeneratePhotoPath')->willReturn('photo.jpg');\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $this->createMock(Client::class),\n $this->createMock(PayloadBuilder::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(CountriesMap::class),\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['handleObjectDeletion'])\n ->getMock();\n\n $service->config = $config;\n $service->profile = $this->createMock(Profile::class);\n\n $team = $this->createMock(Team::class);\n $team->method('getAttribute')->with('id')->willReturn(1);\n $service->team = $team;\n\n $service->method('handleObjectDeletion');\n\n $result = self::invokePrivateMethod('importContact', $service, [$crmData]);\n\n $this->assertNull($result);\n }\n\n public function testImportContactReturnsContactWhenActive(): void\n {\n $crmData = [\n 'Id' => 'SF-CON-ACTIVE',\n 'IsDeleted' => false,\n 'OwnerId' => null,\n 'Name' => 'Active Contact',\n ];\n\n $contact = $this->createMock(Contact::class);\n $contact->method('trashed')->willReturn(false);\n\n $contactsRelation = $this->getMockBuilder(HasMany::class)\n ->disableOriginalConstructor()\n ->addMethods(['where', 'first', 'withTrashed'])\n ->onlyMethods(['updateOrCreate'])\n ->getMock();\n $contactsRelation->method('where')->willReturnSelf();\n $contactsRelation->method('withTrashed')->willReturnSelf();\n $contactsRelation->method('first')->willReturn(null);\n $contactsRelation->method('updateOrCreate')->willReturn($contact);\n\n $config = $this->createMock(Configuration::class);\n $config->method('contacts')->willReturn($contactsRelation);\n $config->method('accounts')->willReturn($contactsRelation);\n\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $prospectPhotoPathService->method('getOrGeneratePhotoPath')->willReturn('photo.jpg');\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $this->createMock(Client::class),\n $this->createMock(PayloadBuilder::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(CountriesMap::class),\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['handleObjectDeletion'])\n ->getMock();\n\n $service->config = $config;\n $service->profile = $this->createMock(Profile::class);\n\n $team = $this->createMock(Team::class);\n $team->method('getAttribute')->with('id')->willReturn(1);\n $service->team = $team;\n\n $service->method('handleObjectDeletion');\n\n $result = self::invokePrivateMethod('importContact', $service, [$crmData]);\n\n $this->assertSame($contact, $result);\n }\n\n public static function resolveContactAccountProvider(): array\n {\n return [\n 'no AccountId returns null' => [[], null],\n 'AccountId present' => [['AccountId' => 'ACC-001'], 'ACC-001'],\n ];\n }\n\n /**\n * @dataProvider resolveContactAccountProvider\n */\n public function testResolveContactAccountWithNoAccountId(array $crmData, ?string $expectedId): void\n {\n $service = $this->getServiceMock(['syncAccount']);\n\n if ($expectedId === null) {\n $service->expects($this->never())->method('syncAccount');\n $config = $this->createMock(Configuration::class);\n $config->expects($this->never())->method('accounts');\n $service->config = $config;\n\n $result = self::invokePrivateMethod('resolveContactAccount', $service, [$crmData]);\n $this->assertNull($result);\n\n return;\n }\n\n $account = $this->createMock(\\Jiminny\\Models\\Account::class);\n\n $accountsRelation = $this->getMockBuilder(HasMany::class)\n ->disableOriginalConstructor()\n ->addMethods(['where', 'first'])\n ->getMock();\n $accountsRelation->method('where')->with('crm_provider_id', $expectedId)->willReturnSelf();\n $accountsRelation->method('first')->willReturn($account);\n\n $config = $this->createMock(Configuration::class);\n $config->method('accounts')->willReturn($accountsRelation);\n $service->config = $config;\n\n $service->expects($this->never())->method('syncAccount');\n\n $result = self::invokePrivateMethod('resolveContactAccount', $service, [$crmData]);\n $this->assertSame($account, $result);\n }\n\n public function testResolveContactAccountSyncsWhenNotFoundLocally(): void\n {\n $syncedAccount = $this->createMock(\\Jiminny\\Models\\Account::class);\n\n $accountsRelation = $this->getMockBuilder(HasMany::class)\n ->disableOriginalConstructor()\n ->addMethods(['where', 'first'])\n ->getMock();\n $accountsRelation->method('where')->willReturnSelf();\n $accountsRelation->method('first')->willReturn(null);\n\n $config = $this->createMock(Configuration::class);\n $config->method('accounts')->willReturn($accountsRelation);\n\n $service = $this->getServiceMock(['syncAccount']);\n $service->config = $config;\n\n $service->expects($this->once())\n ->method('syncAccount')\n ->with('ACC-MISSING')\n ->willReturn($syncedAccount);\n\n $result = self::invokePrivateMethod('resolveContactAccount', $service, [['AccountId' => 'ACC-MISSING']]);\n\n $this->assertSame($syncedAccount, $result);\n }\n\n public static function resolveContactCountryCodeProvider(): array\n {\n return [\n 'valid MailingCountryCode' => [['MailingCountryCode' => 'GB'], true, null, 'GB'],\n 'invalid MailingCountryCode falls to null' => [['MailingCountryCode' => 'XX'], false, null, null],\n 'no code, uses MailingCountry converted' => [['MailingCountry' => 'Germany'], null, 'DE', 'DE'],\n 'no code, country name null, uses account' => [['MailingCountry' => 'Unknown'], null, null, 'US'],\n 'no code, no country at all' => [[], null, null, null],\n ];\n }\n\n /**\n * @dataProvider resolveContactCountryCodeProvider\n */\n public function testResolveContactCountryCode(\n array $crmData,\n ?bool $countryExists,\n ?string $convertedCode,\n ?string $expected\n ): void {\n $countriesMap = $this->createMock(CountriesMap::class);\n if ($countryExists !== null) {\n $countriesMap->method('countryExists')->willReturn($countryExists);\n }\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $this->createMock(Client::class),\n $this->createMock(PayloadBuilder::class),\n $this->createMock(Dispatcher::class),\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods(['convertCountryNameToCode'])\n ->getMock();\n\n $service->profile = $this->createMock(Profile::class);\n\n if (isset($crmData['MailingCountry'])) {\n $service->expects($this->once())\n ->method('convertCountryNameToCode')\n ->with($crmData['MailingCountry'])\n ->willReturn($convertedCode);\n } else {\n $service->expects($this->never())->method('convertCountryNameToCode');\n }\n\n $account = null;\n if ($expected === 'US') {\n $account = new \\Jiminny\\Models\\Account();\n $account->setAttribute('country_code', 'US');\n }\n\n $result = self::invokePrivateMethod('resolveContactCountryCode', $service, [$crmData, $account]);\n\n $this->assertSame($expected, $result);\n }\n\n public static function parseContactPhoneProvider(): array\n {\n return [\n 'empty Phone returns empty' => [['Phone' => ''], null, [[], null]],\n 'no Phone key returns empty' => [[], null, [[], null]],\n ];\n }\n\n /**\n * @dataProvider parseContactPhoneProvider\n */\n public function testParseContactPhoneWithEmptyPhone(array $crmData, ?string $countryCode, array $expected): void\n {\n $service = $this->getServiceMock();\n $result = self::invokePrivateMethod('parseContactPhone', $service, [$countryCode, $crmData]);\n $this->assertSame($expected, $result);\n }\n\n public function testParseContactPhoneWithValidNumber(): void\n {\n $service = $this->getServiceMock();\n $crmData = ['Phone' => '+12025551234'];\n\n [$parsedNumber, $ext] = self::invokePrivateMethod('parseContactPhone', $service, [null, $crmData]);\n\n $this->assertArrayHasKey('phone', $parsedNumber);\n $this->assertSame('+12025551234', $parsedNumber['phone']);\n $this->assertNull($ext);\n }\n\n public function testParseContactPhoneWithExtension(): void\n {\n $service = $this->getServiceMock();\n $crmData = ['Phone' => '+12025551234 ext. 42'];\n\n [$parsedNumber, $ext] = self::invokePrivateMethod('parseContactPhone', $service, ['US', $crmData]);\n\n $this->assertArrayHasKey('phone', $parsedNumber);\n $this->assertSame('42', $ext);\n }\n\n public function testParseContactPhoneExtIsTruncatedTo10Chars(): void\n {\n $service = $this->getServiceMock();\n $crmData = ['Phone' => '+12025551234 ext. 12345678901'];\n\n [$parsedNumber, $ext] = self::invokePrivateMethod('parseContactPhone', $service, ['US', $crmData]);\n\n $this->assertNotNull($ext);\n $this->assertLessThanOrEqual(10, strlen($ext));\n }\n\n public function testParseContactPhoneWithCountryCode(): void\n {\n $service = $this->getServiceMock();\n $crmData = ['Phone' => '02075551234'];\n\n [$parsedNumber, $ext] = self::invokePrivateMethod('parseContactPhone', $service, ['GB', $crmData]);\n\n $this->assertArrayHasKey('phone', $parsedNumber);\n $this->assertStringStartsWith('+44', $parsedNumber['phone']);\n $this->assertNull($ext);\n }\n\n public static function parseContactMobileProvider(): array\n {\n return [\n 'empty MobilePhone returns null' => [['MobilePhone' => ''], null, null],\n 'no MobilePhone key returns null' => [[], null, null],\n ];\n }\n\n /**\n * @dataProvider parseContactMobileProvider\n */\n public function testParseContactMobileWithEmptyPhone(array $crmData, ?string $countryCode, ?string $expected): void\n {\n $service = $this->getServiceMock();\n $result = self::invokePrivateMethod('parseContactMobile', $service, [$countryCode, $crmData]);\n $this->assertSame($expected, $result);\n }\n\n public function testParseContactMobileWithValidNumber(): void\n {\n $service = $this->getServiceMock();\n $crmData = ['MobilePhone' => '+12025550199'];\n\n $result = self::invokePrivateMethod('parseContactMobile', $service, [null, $crmData]);\n\n $this->assertNotNull($result);\n $this->assertSame('+12025550199', $result);\n }\n\n public function testParseContactMobileIsTruncatedTo25Chars(): void\n {\n $service = $this->getServiceMock();\n $longNumber = '+1202555019912345678901234';\n $crmData = ['MobilePhone' => $longNumber];\n\n $result = self::invokePrivateMethod('parseContactMobile', $service, [null, $crmData]);\n\n $this->assertNotNull($result);\n $this->assertLessThanOrEqual(25, strlen($result));\n }\n\n public function testParseContactMobileWithCountryCode(): void\n {\n $service = $this->getServiceMock();\n $crmData = ['MobilePhone' => '07911123456'];\n\n $result = self::invokePrivateMethod('parseContactMobile', $service, ['GB', $crmData]);\n\n $this->assertNotNull($result);\n $this->assertStringStartsWith('+44', $result);\n }\n\n public function testImportOpportunitySkipsWhenProfileNotFound(): void\n {\n $crmData = [\n 'Id' => 'SF-NO-USER-2',\n 'Name' => 'Test Opportunity',\n 'OwnerId' => 'owner-not-found',\n // No AccountId - avoid complex account processing\n ];\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $eventDispatcher = $this->createMock(\\Illuminate\\Events\\Dispatcher::class); // ← ADD THIS\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n\n\n $service = new Service(\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService\n );\n\n $config = $this->createMock(Configuration::class);\n\n // Mock profiles relation returning null (no profile found)\n $profilesRelation = \\Mockery::mock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $profilesRelation->shouldReceive('where')->with('crm_provider_id', 'owner-not-found')->andReturnSelf();\n $profilesRelation->shouldReceive('first')->andReturn(null);\n\n $config->expects($this->once())\n ->method('profiles')\n ->willReturn($profilesRelation);\n\n $team = $this->createMock(Team::class);\n $team->method('getId')->willReturn(1);\n\n $logger = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $logger->expects($this->once())\n ->method('error')\n ->with(\n '[Salesforce] | Skip import, no user_id found',\n ['id' => 'SF-NO-USER-2']\n );\n\n $reflection = new \\ReflectionClass($service);\n\n $configProperty = $reflection->getProperty('config');\n $configProperty->setAccessible(true);\n $configProperty->setValue($service, $config);\n\n $teamProperty = $reflection->getProperty('team');\n $teamProperty->setAccessible(true);\n $teamProperty->setValue($service, $team);\n\n $loggerProperty = $reflection->getProperty('logger');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($service, $logger);\n\n // Initialize profile property\n $profileProperty = $reflection->getProperty('profile');\n $profileProperty->setAccessible(true);\n $profileProperty->setValue($service, null);\n\n $result = self::invokePrivateMethod('importOpportunity', $service, [$crmData]);\n\n $this->assertNull($result);\n }\n}\n\nclass HasManyExtended extends HasMany\n{\n public function where()\n {\n }\n\n public function first()\n {\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Crm\\Salesforce;\n\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Events\\Dispatcher;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Jiminny\\Component\\Country\\CountriesMap;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Enums\\CrmObject;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\NoResultsException;\nuse Jiminny\\Integrations\\PlaybookResolver;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\TeamSettings;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Salesforce\\Client;\nuse Jiminny\\Services\\Crm\\Salesforce\\PayloadBuilder;\nuse Jiminny\\Services\\Crm\\Salesforce\\QueryBuilder;\nuse Jiminny\\Services\\Crm\\Salesforce\\QueryHandler;\nuse Jiminny\\Services\\Crm\\Salesforce\\QueryIterator;\nuse Jiminny\\Services\\Crm\\Salesforce\\QueryResults;\nuse Jiminny\\Services\\Crm\\Salesforce\\Service;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\nuse Tests\\Unit\\Traits\\TestPrivateMethod;\n\nclass ServiceTest extends TestCase\n{\n use TestPrivateMethod;\n\n public function testFetchAndAssociateRelatedActivity(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $payloadBuilder->method('addCustomLogicFieldsPayload')\n ->willReturnCallback(function ($activity, $payload) {\n return $payload;\n });\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods(['fetchRelatedActivity', 'getPlaybook', 'getPlaybookCategory', 'updateRecord'])\n ->getMock();\n\n $serviceMock->expects($this->once())\n ->method('fetchRelatedActivity')\n ->willReturn([\n 'Id' => 'testId',\n 'Type' => null,\n 'OwnerId' => 'testerUser',\n 'Description' => 'Test description',\n ]);\n\n $user = $this->createMock(User::class);\n $team = $this->createMock(Team::class);\n $user->method('getAttribute')->with('team')->willReturn($team);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityField')->willReturn(null);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_EVENT);\n\n $serviceMock->expects($this->once())\n ->method('getPlaybook')\n ->with($user)\n ->willReturn($playbook);\n\n $serviceMock->expects($this->never())\n ->method('getPlaybookCategory');\n\n $serviceMock->expects($this->never())\n ->method('updateRecord');\n\n $fieldDataRepository = $this->createMock(FieldDataRepository::class);\n $fieldDataRepository->method('getActivityFieldData')->willReturn(collect([]));\n app()->instance(FieldDataRepository::class, $fieldDataRepository);\n\n $config = $this->createMock(Configuration::class);\n $profilesRelation = $this->getMockBuilder(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class)\n ->disableOriginalConstructor()\n ->onlyMethods(['get'])\n ->addMethods(['where'])\n ->getMock();\n $profilesRelation->method('where')->willReturnSelf();\n $profilesRelation->method('get')->willReturn(collect([]));\n $config->method('profiles')->willReturn($profilesRelation);\n\n $serviceMock->config = $config;\n $serviceMock->profile = null;\n\n $actualStartTime = \\Carbon\\Carbon::now();\n\n $activity = $this->getMockBuilder(Activity::class)\n ->disableOriginalConstructor()\n ->onlyMethods(['update', 'hasProspect'])\n ->getMock();\n\n $activity->method('update')->willReturn(true);\n $activity->method('hasProspect')->willReturn(true);\n\n $activity->type = Activity::TYPE_CONFERENCE;\n $activity->provider = Activity::PROVIDER_TWILIO;\n $activity->lead_id = 1;\n $activity->user_id = 0;\n $activity->id_string = 'test-activity-id';\n $activity->user = $user;\n\n $activity->actual_start_time = $actualStartTime;\n $activity->uuid = 'c53d8320-f556-4cee-a2f8-5f232f454ca4';\n\n app()->bind(PlaybookResolver::class, function () use ($user) {\n $playbook = $this->createMock(Playbook::class);\n $playbookResolver = $this->createMock(PlaybookResolver::class);\n $playbookResolver->expects($this->once())\n ->method('resolvePlaybookByUser')\n ->with($user)\n ->willReturn($playbook);\n\n return $playbookResolver;\n });\n\n $data = $serviceMock->fetchAndAssociateRelatedActivity($activity);\n\n $this->assertInstanceOf(Activity::class, $data);\n $this->assertEquals(Activity::TYPE_CONFERENCE, $data->getType());\n $this->assertEquals($actualStartTime->getTimestamp(), $data->getActualStartTime()->getTimestamp());\n }\n\n public function testFetchAndAssociateRelatedActivitySkipsForTaskBasedPlaybook(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods(['fetchRelatedActivity', 'getPlaybook'])\n ->getMock();\n\n $user = $this->createMock(User::class);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n $playbook->method('getId')->willReturn(123);\n\n $serviceMock->expects($this->once())\n ->method('getPlaybook')\n ->with($user)\n ->willReturn($playbook);\n\n $serviceMock->expects($this->never())\n ->method('fetchRelatedActivity');\n\n $activity = $this->getMockBuilder(Activity::class)\n ->disableOriginalConstructor()\n ->onlyMethods(['hasProspect', 'getUuid'])\n ->getMock();\n\n $activity->method('hasProspect')->willReturn(true);\n $activity->method('getUuid')->willReturn('c53d8320-f556-4cee-a2f8-5f232f454ca4');\n $activity->type = Activity::TYPE_CONFERENCE;\n $activity->actual_start_time = \\Carbon\\Carbon::now();\n $activity->user = $user;\n\n $result = $serviceMock->fetchAndAssociateRelatedActivity($activity);\n\n $this->assertNull($result);\n }\n\n public function testFetchAndAssociateRelatedActivityReturnsNullForNonConference(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $serviceMock = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class)\n );\n\n $activity = $this->getMockBuilder(Activity::class)\n ->disableOriginalConstructor()\n ->getMock();\n\n $activity->type = Activity::TYPE_SOFTPHONE;\n\n $result = $serviceMock->fetchAndAssociateRelatedActivity($activity);\n\n $this->assertNull($result);\n }\n\n public function testFetchAndAssociateRelatedActivityReturnsNullWhenNoStartTime(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $serviceMock = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class)\n );\n\n\n $activity = $this->getMockBuilder(Activity::class)\n ->disableOriginalConstructor()\n ->getMock();\n\n $activity->type = Activity::TYPE_CONFERENCE;\n $activity->actual_start_time = null;\n $activity->scheduled_start_time = null;\n\n $result = $serviceMock->fetchAndAssociateRelatedActivity($activity);\n\n $this->assertNull($result);\n }\n\n public function testFetchAndAssociateRelatedActivityReturnsNullWhenNoProspect(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods(['getPlaybook'])\n ->getMock();\n\n $serviceMock->expects($this->never())\n ->method('getPlaybook');\n\n $activity = $this->getMockBuilder(Activity::class)\n ->disableOriginalConstructor()\n ->onlyMethods(['hasProspect', 'getUuid'])\n ->getMock();\n\n $activity->method('hasProspect')->willReturn(false);\n $activity->method('getUuid')->willReturn('c53d8320-f556-4cee-a2f8-5f232f454ca4');\n $activity->type = Activity::TYPE_CONFERENCE;\n $activity->actual_start_time = \\Carbon\\Carbon::now();\n\n $result = $serviceMock->fetchAndAssociateRelatedActivity($activity);\n\n $this->assertNull($result);\n }\n\n public function testMatchExactlyByEmail(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods([])\n ->getMock();\n\n $profile = new Profile();\n $profile->setAttribute('id', bin2hex(random_bytes(8)));\n $serviceMock->profile = $profile;\n\n $team = $this->createMock(Team::class);\n $serviceMock->team = $team;\n\n $data = $serviceMock->matchExactlyByEmail(bin2hex(random_bytes(8)) . 'test_email@testserver.com');\n\n $this->assertEquals(null, $data);\n }\n\n public function testMatchDomainFromEmail(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $queryIterator = $this->createMock(QueryIterator::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $config = $this->createMock(Configuration::class);\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->any())\n ->method('search')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods(['convertCrmData'])\n ->getMock();\n\n $profile = new Profile();\n $profile->account_fields = 'Field1, Field2, Field3';\n $serviceMock->profile = $profile;\n\n $serviceMock->expects($this->once())\n ->method('convertCrmData')\n ->willReturn(['test']);\n\n $this->app->bind(QueryBuilder::class, function () {\n $queryBuilder = $this->createMock(QueryBuilder::class);\n $queryBuilder->expects($this->once())\n ->method('buildMatchByDomainQuery')\n ->with('test_email@testserver.com')\n ->willReturn('FIND {test_email@testserver.com} IN ALL FIELDS RETURNING Account(Id)');\n\n return $queryBuilder;\n });\n\n $team = $this->createMock(Team::class);\n\n $serviceMock->team = $team;\n $serviceMock->config = $config;\n\n $data = $serviceMock->matchByDomain('test_email@testserver.com');\n\n $this->assertEquals(['test'], $data);\n }\n\n public function testBuildTaskSearchFields(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class)\n );\n\n $fields = $service->buildTaskSearchFields();\n\n $expectedFields = ['Id', 'WhoId', 'WhatId', 'AccountId'];\n\n $this->assertEquals($expectedFields, $fields);\n }\n\n public function testMapCrmObjects(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class),\n );\n\n $sampleTask = [\n 'WhoId' => '003sampleWhoId',\n 'AccountId' => 'sampleAccountId',\n 'WhatId' => 'sampleWhatId',\n ];\n\n $activityData = $service->mapCrmObjects($sampleTask);\n\n $expectedActivityData = [\n 'contact' => '003sampleWhoId',\n 'account' => 'sampleAccountId',\n 'opportunity' => 'sampleWhatId',\n ];\n\n $this->assertEquals($expectedActivityData, $activityData);\n }\n\n public function testGetInstalledAppVersion(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n $queryIterator = $this->createMock(QueryIterator::class);\n $queryIterator->expects($this->any())\n ->method('current')->willReturn([\n 'SubscriberPackageVersion' => [\n 'MajorVersion' => '1',\n 'MinorVersion' => '0',\n 'PatchVersion' => '1',\n 'BuildNumber' => '0',\n ],\n ]);\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->any())\n ->method('metadata')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods(array_diff(get_class_methods(Service::class), ['getInstalledAppVersion']))\n ->getMock();\n\n $version = $serviceMock->getInstalledAppVersion();\n\n $this->assertEquals('1010', $version);\n }\n\n public function testSyncProfiles(): void\n {\n $userToSearch = null;\n\n $team = $this->createMock(Team::class);\n $config = $this->createMockedConfiguration();\n $config->expects($this->once())\n ->method('getId')\n ->willReturn(1);\n\n $salesforceUser = [\n 'Email' => 'test@example.com',\n 'UserPreferencesLightningExperiencePreferred' => true,\n 'CallCenterId' => '123',\n 'Id' => '456',\n 'ProfileId' => '789',\n ];\n\n app()->bind(QueryBuilder::class, function () use ($userToSearch) {\n $queryBuilder = $this->createMock(QueryBuilder::class);\n $queryBuilder->expects($this->once())\n ->method('buildGetUsersQuery')\n ->with($userToSearch)\n ->willReturn('SELECT * FROM Users');\n\n return $queryBuilder;\n });\n\n $queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults([$salesforceUser], 1, true, null));\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->any())\n ->method('query')\n ->with('SELECT * FROM Users')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n\n $user = $this->createMock(User::class);\n $user->expects($this->once())\n ->method('getId')\n ->willReturn(123);\n\n $this->mockTeamRepository($team, $salesforceUser, $user);\n\n $profileRepository = $this->createMock(ProfileRepository::class);\n $profileRepository->expects($this->once())\n ->method('updateOrCreateProfile')\n ->with(\n $user,\n [\n 'crm_configuration_id' => 1,\n 'crm_provider_id' => '456',\n ],\n [\n 'user_id' => 123,\n 'edition' => Profile::EDITION_LIGHTNING,\n 'has_external_cti' => true,\n 'crm_profile_id' => '789',\n ]\n )\n ->willReturn(new Profile());\n\n $this->app->instance(ProfileRepository::class, $profileRepository);\n\n $serviceMock = $this->getServiceMock();\n $serviceMock->team = $team;\n $serviceMock->config = $config;\n $result = $serviceMock->syncProfiles($userToSearch);\n\n $this->assertNull($result);\n }\n\n public function testSyncProfilesEmailIsNull(): void\n {\n $userToSearch = $this->createMock(User::class);\n\n $salesforceUser = [\n 'Email' => null,\n ];\n\n app()->bind(QueryBuilder::class, function () {\n $queryBuilder = $this->createMock(QueryBuilder::class);\n $queryBuilder->expects($this->once())\n ->method('buildGetUsersQuery')\n ->with(null)\n ->willReturn('SELECT * FROM Users');\n\n return $queryBuilder;\n });\n\n $queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults([$salesforceUser], 1, true, null));\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->exactly(2))\n ->method('query')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n\n $team = $this->createMock(Team::class);\n $user = $this->createMock(User::class);\n $this->mockTeamRepository($team, $salesforceUser, $user, false);\n\n $config = $this->createMock(Configuration::class);\n\n $serviceMock = $this->getServiceMock();\n $serviceMock->team = $team;\n $serviceMock->config = $config;\n\n $profile = $serviceMock->syncProfiles(null);\n\n $this->assertNull($profile);\n }\n\n public function testSyncProfilesUserToSearchMatchesCurrentUser(): void\n {\n $userToSearch = $this->createMock(User::class);\n $userToSearch->expects($this->once())\n ->method('getId')\n ->willReturn(123);\n\n $team = $this->createMock(Team::class);\n $config = $this->createMock(Configuration::class);\n $config->expects($this->once())\n ->method('getId')\n ->willReturn(1);\n\n $salesforceUser = [\n 'Email' => 'test@example.com',\n 'UserPreferencesLightningExperiencePreferred' => true,\n 'CallCenterId' => '123',\n 'Id' => '456',\n 'ProfileId' => '789',\n ];\n\n app()->bind(QueryBuilder::class, function () use ($userToSearch) {\n $queryBuilder = $this->createMock(QueryBuilder::class);\n $queryBuilder->expects($this->once())\n ->method('buildGetUsersQuery')\n ->with($userToSearch)\n ->willReturn('SELECT * FROM Users');\n\n return $queryBuilder;\n });\n\n $queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults([$salesforceUser], 1, true, null));\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->any())\n ->method('query')\n ->with('SELECT * FROM Users')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n\n $user = $this->createMock(User::class);\n $user->expects($this->exactly(2))\n ->method('getId')\n ->willReturn(123);\n\n $this->mockTeamRepository($team, $salesforceUser, $user);\n\n $profileRepository = $this->createMock(ProfileRepository::class);\n $profileRepository->expects($this->once())\n ->method('updateOrCreateProfile')\n ->with(\n $user,\n [\n 'crm_configuration_id' => 1,\n 'crm_provider_id' => '456',\n ],\n [\n 'user_id' => 123,\n 'edition' => Profile::EDITION_LIGHTNING,\n 'has_external_cti' => true,\n 'crm_profile_id' => '789',\n ]\n )\n ->willReturn(new Profile());\n\n $this->app->instance(ProfileRepository::class, $profileRepository);\n\n $serviceMock = $this->getServiceMock();\n $serviceMock->team = $team;\n $serviceMock->config = $config;\n $profile = $serviceMock->syncProfiles($userToSearch);\n\n $this->assertInstanceOf(Profile::class, $profile);\n }\n\n public function testSyncProfilesWithCustomValidation(): void\n {\n $userToSearch = null;\n\n $team = $this->createMock(Team::class);\n $config = $this->createMockedConfiguration();\n $config->expects($this->atLeastOnce()) // Changed from once() to atLeastOnce()\n ->method('getId')\n ->willReturn(1);\n\n $salesforceUser = [\n 'Email' => 'test@example.com',\n 'UserPreferencesLightningExperiencePreferred' => true,\n 'CallCenterId' => '123',\n 'Id' => '456',\n 'ProfileId' => '789',\n 'CustomField' => 'CustomValue',\n ];\n\n $customRules = [\n ['field' => 'CustomField', 'value' => 'CustomValue'],\n ];\n\n $this->mockQueryBuilderAndHandler($userToSearch, [$salesforceUser]);\n\n $user = $this->createMock(User::class);\n $user->expects($this->once())\n ->method('getId')\n ->willReturn(123);\n\n $this->mockTeamRepository($team, $salesforceUser, $user, true, $customRules);\n\n $this->mockProfileRepository($user);\n\n $serviceMock = $this->getServiceMock();\n $serviceMock->team = $team;\n $serviceMock->config = $config;\n $profile = $serviceMock->syncProfiles($userToSearch);\n\n $this->assertNull($profile);\n }\n\n public function testSyncProfilesWithCustomValidationFailing(): void\n {\n $userToSearch = null;\n\n $team = $this->createMock(Team::class);\n $config = $this->createMockedConfiguration();\n\n $salesforceUser = [\n 'Email' => 'test@example.com',\n 'UserPreferencesLightningExperiencePreferred' => true,\n 'CallCenterId' => '123',\n 'Id' => '456',\n 'ProfileId' => '789',\n 'CustomField' => 'WrongValue',\n ];\n\n $customRules = [\n ['field' => 'CustomField', 'value' => 'CustomValue'],\n ];\n\n $this->mockQueryBuilderAndHandler($userToSearch, [$salesforceUser]);\n\n $teamRepository = $this->getMockForAbstractClass(TeamRepository::class, [], '', false, true, true, ['findActiveTeamMemberByEmail', 'getTeamSetting']);\n\n $teamSettings = $this->createMock(TeamSettings::class);\n $teamSettings->method('getValueType')\n ->willReturn('array');\n\n $teamSettings->method('getValue')\n ->willReturn(json_encode($customRules));\n\n $teamRepository->expects($this->once())\n ->method('getTeamSetting')\n ->with($team, 'custom_profile_validation')\n ->willReturn($teamSettings);\n\n app()->bind(TeamRepository::class, function () use ($teamRepository) {\n return $teamRepository;\n });\n\n $profileRepository = $this->createMock(ProfileRepository::class);\n $profileRepository->expects($this->never())\n ->method('updateOrCreateProfile');\n\n $this->app->instance(ProfileRepository::class, $profileRepository);\n\n $serviceMock = $this->getServiceMock();\n $serviceMock->team = $team;\n $serviceMock->config = $config;\n $result = $serviceMock->syncProfiles($userToSearch);\n\n $this->assertNull($result);\n }\n\n public function testGetContactRolesFromCrm(): void\n {\n $contactRoles = [\n [\n 'Id' => '1',\n 'ContactId' => 'Contact1',\n 'OpportunityId' => 'Opportunity1',\n 'Opportunity' => ['OwnerId' => 'Owner1'],\n 'IsPrimary' => true,\n 'Role' => 'Decision Maker',\n ],\n ];\n\n $expectedResponse = [\n [\n 'id' => '1',\n 'contactId' => 'Contact1',\n 'opportunityId' => 'Opportunity1',\n 'ownerId' => 'Owner1',\n 'isPrimary' => true,\n 'role' => 'Decision Maker',\n ],\n ];\n\n $this->bindQueryIterator($contactRoles);\n\n $serviceMock = $this->getServiceMock();\n\n $data = $serviceMock->getContactRolesFromCrm(now()->subDay());\n\n $this->assertEquals($expectedResponse, $data);\n }\n\n public function testGetContactRolesFromCrmNoResult(): void\n {\n $this->bindQueryIterator([]);\n\n $serviceMock = $this->getServiceMock();\n\n $data = $serviceMock->getContactRolesFromCrm(now()->subDay());\n\n $this->assertEquals([], $data);\n }\n\n public function testSyncContactRoles(): void\n {\n $contactRoles = [\n [\n 'id' => '1',\n 'contactId' => 'Contact1',\n 'opportunityId' => 'Opportunity1',\n 'ownerId' => 'Owner1',\n 'isPrimary' => true,\n 'role' => 'Decision Maker',\n ],\n ];\n\n app()->bind(ContactRoleRepository::class, function () {\n $contactRoleRepository = $this->createMock(ContactRoleRepository::class);\n $contactRoleRepository->expects($this->once())\n ->method('saveContactRoles');\n\n return $contactRoleRepository;\n });\n\n $serviceMock = $this->getServiceMock([\n 'getContactRolesFromCrm',\n 'syncRemotelyDeletedContactRoles',\n 'syncContact',\n 'syncOpportunity',\n ]);\n\n $config = $this->createMock(Configuration::class);\n $hasMany = $this->createMock(HasManyExtended::class);\n $hasMany->expects($this->exactly(2))\n ->method('where')\n ->willReturn($hasMany);\n\n $hasMany->expects($this->exactly(2))\n ->method('first')\n ->willReturn(\n $this->createMock(Contact::class),\n $this->createMock(Opportunity::class)\n );\n\n $config->expects($this->once())\n ->method('contacts')\n ->willReturn($hasMany);\n $config->expects($this->once())\n ->method('opportunities')\n ->willReturn($hasMany);\n\n $serviceMock->config = $config;\n\n $serviceMock->expects($this->once())\n ->method('getContactRolesFromCrm')\n ->willReturn($contactRoles);\n\n $serviceMock->expects($this->once())\n ->method('syncRemotelyDeletedContactRoles');\n\n $data = $serviceMock->syncContactRoles(now()->subDay());\n\n $this->assertEquals(1, $data);\n }\n\n public function testSyncRemotelyDeletedContactRoles(): void\n {\n $contactRoles = [\n [\n 'id' => '1',\n 'crm_provider_id' => '1',\n ],\n ];\n\n app()->bind(QueryHandler::class, function () use ($contactRoles) {\n $queryResults = new QueryResults($contactRoles, 1, true, null);\n\n $handler = $this->createMock(QueryHandler::class);\n $handler->method('queryDeleted')\n ->willReturn($queryResults);\n\n return $handler;\n });\n\n app()->bind(ContactRoleRepository::class, function () {\n $contactRoleRepository = $this->createMock(ContactRoleRepository::class);\n $contactRoleRepository->expects($this->once())\n ->method('deleteContactRoles');\n\n return $contactRoleRepository;\n });\n\n $serviceMock = $this->getServiceMock();\n $serviceMock->team = $this->createMock(Team::class);\n\n $data = $this->invokePrivateMethod('syncRemotelyDeletedContactRoles', $serviceMock, []);\n\n $this->assertTrue($data);\n }\n\n private function bindQueryIterator(array $queryResult): void\n {\n /** @var Client $client */\n $client = $this->createMock(Client::class);\n $queryIterator = new QueryIterator(\n $client,\n new QueryResults($queryResult, 1, true, null)\n );\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->any())\n ->method('query')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n }\n\n public static function getOpportunitySortOrderDataProvider(): array\n {\n return [\n 'all open recently updated' => [Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED, ['LastModifiedDate DESC', true]],\n 'all open recently created' => [Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED, ['CreatedDate DESC', true]],\n 'all open oldest created' => [Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED, ['CreatedDate ASC', true]],\n 'all recently updated' => [Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED, ['LastModifiedDate DESC', false]],\n 'default' => ['unknown', ['LastModifiedDate DESC', true]],\n ];\n }\n\n private function createMockedConfiguration(): Configuration\n {\n $config = $this->createMock(Configuration::class);\n $profilesRelation = $this->getMockBuilder(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class)\n ->disableOriginalConstructor()\n ->onlyMethods(['get'])\n ->addMethods(['where', 'first'])\n ->getMock();\n $profilesRelation->method('where')->willReturnSelf();\n $profilesRelation->method('get')->willReturn(collect([]));\n $profilesRelation->method('first')->willReturn(null);\n $config->method('profiles')->willReturn($profilesRelation);\n\n return $config;\n }\n\n private function getServiceMock(array $onlyMethods = []): MockObject&Service\n {\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $this->createMock(Client::class),\n $this->createMock(PayloadBuilder::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(CountriesMap::class),\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods($onlyMethods)\n ->getMock();\n\n $serviceMock->profile = $this->createMock(Profile::class);\n\n return $serviceMock;\n }\n\n private function mockQueryBuilderAndHandler($userToSearch, $salesforceUsers): void\n {\n app()->bind(QueryBuilder::class, function () use ($userToSearch) {\n $queryBuilder = $this->createMock(QueryBuilder::class);\n $queryBuilder->expects($this->once())\n ->method('buildGetUsersQuery')\n ->with($userToSearch)\n ->willReturn('SELECT * FROM Users');\n\n return $queryBuilder;\n });\n\n $queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults($salesforceUsers, count($salesforceUsers), true, null));\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->any())\n ->method('query')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n }\n\n private function mockTeamRepository(\n Team $team,\n array $salesforceUser,\n ?User $user = null,\n bool $userSearch = true,\n array $customRules = []\n ): void {\n $teamRepository = $this->getMockForAbstractClass(TeamRepository::class, [], '', false, true, true, ['findActiveTeamMemberByEmail', 'getTeamSetting']);\n\n if ($userSearch) {\n $teamRepository->expects($this->once())\n ->method('findActiveTeamMemberByEmail')\n ->with($team, $salesforceUser['Email'])\n ->willReturn($user);\n }\n\n $teamSettings = $this->createMock(TeamSettings::class);\n $teamSettings->method('getValueType')\n ->willReturn('array');\n\n $teamSettings->method('getValue')\n ->willReturn(json_encode($customRules));\n\n $teamRepository->expects($this->once())\n ->method('getTeamSetting')\n ->with($team, 'custom_profile_validation')\n ->willReturn($teamSettings);\n\n app()->bind(TeamRepository::class, function () use ($teamRepository) {\n return $teamRepository;\n });\n }\n\n private function mockProfileRepository(User $user): void\n {\n $profileRepository = $this->createMock(ProfileRepository::class);\n $profileRepository->expects($this->once())\n ->method('updateOrCreateProfile')\n ->with(\n $user,\n [\n 'crm_configuration_id' => 1,\n 'crm_provider_id' => '456',\n ],\n [\n 'user_id' => 123,\n 'edition' => Profile::EDITION_LIGHTNING,\n 'has_external_cti' => true,\n 'crm_profile_id' => '789',\n ]\n )\n ->willReturn(new Profile());\n\n $this->app->instance(ProfileRepository::class, $profileRepository);\n }\n\n public function testBuildEnhancedNoteDecodesHtmlEntities(): void\n {\n $service = $this->getServiceMock(['createRecord']);\n\n $profile = new Profile();\n $profile->setAttribute('crm_provider_id', 'owner-123');\n $service->profile = $profile;\n\n $service->expects($this->exactly(2))\n ->method('createRecord')\n ->willReturnOnConsecutiveCalls('note-id-123', 'link-id-456');\n\n $bodyWithEntities = 'Welch's current challenges and Facebook's Club';\n\n $result = $this->invokePrivateMethod('buildEnhancedNote', $service, [\n 'Test Title',\n $bodyWithEntities,\n 'object-id-789',\n ]);\n\n $this->assertEquals('note-id-123', $result);\n }\n\n public function testBuildEnhancedNoteSanitizesWithoutQuotes(): void\n {\n $service = $this->getServiceMock(['createRecord']);\n\n $profile = new Profile();\n $profile->setAttribute('crm_provider_id', 'owner-456');\n $service->profile = $profile;\n\n $service->expects($this->exactly(2))\n ->method('createRecord')\n ->willReturnCallback(function ($type, $data) {\n if ($type === 'ContentNote') {\n $decoded = base64_decode($data['Content']);\n $this->assertStringContainsString(\"Welch's\", $decoded);\n $this->assertStringNotContainsString(''', $decoded);\n $this->assertStringNotContainsString('&#039;', $decoded);\n $this->assertStringContainsString('<script>', $decoded);\n\n return 'note-id-456';\n }\n\n return 'link-id-789';\n });\n\n $bodyWithMixedContent = \"Welch's and <script>alert('xss')</script>\";\n\n $result = $this->invokePrivateMethod('buildEnhancedNote', $service, [\n 'Test Title',\n $bodyWithMixedContent,\n 'object-id-123',\n ]);\n\n $this->assertEquals('note-id-456', $result);\n }\n\n public function testBuildEnhancedNoteConvertsLineBreaks(): void\n {\n $service = $this->getServiceMock(['createRecord']);\n\n $profile = new Profile();\n $profile->setAttribute('crm_provider_id', 'owner-789');\n $service->profile = $profile;\n\n $service->expects($this->exactly(2))\n ->method('createRecord')\n ->willReturnCallback(function ($type, $data) {\n if ($type === 'ContentNote') {\n $decoded = base64_decode($data['Content']);\n $this->assertStringContainsString('<br>', $decoded);\n $this->assertStringNotContainsString('<br />', $decoded);\n\n return 'note-id-789';\n }\n\n return 'link-id-012';\n });\n\n $bodyWithLineBreaks = \"Line 1\\nLine 2\\nLine 3\";\n\n $result = $this->invokePrivateMethod('buildEnhancedNote', $service, [\n 'Test Title',\n $bodyWithLineBreaks,\n 'object-id-456',\n ]);\n\n $this->assertEquals('note-id-789', $result);\n }\n\n public function testBuildEnhancedNoteHandlesComplexScenario(): void\n {\n $service = $this->getServiceMock(['createRecord']);\n\n $profile = new Profile();\n $profile->setAttribute('crm_provider_id', 'owner-complex');\n $service->profile = $profile;\n\n $service->expects($this->exactly(2))\n ->method('createRecord')\n ->willReturnCallback(function ($type, $data) {\n if ($type === 'ContentNote') {\n $decoded = base64_decode($data['Content']);\n\n $this->assertStringContainsString(\"Welch's\", $decoded);\n $this->assertStringContainsString(\"Facebook's Club\", $decoded);\n $this->assertStringContainsString(\"Arctics'\", $decoded);\n $this->assertStringNotContainsString(''', $decoded);\n $this->assertStringNotContainsString('&#039;', $decoded);\n $this->assertStringContainsString('<br>', $decoded);\n $this->assertStringContainsString('<', $decoded);\n $this->assertStringContainsString('>', $decoded);\n\n return 'note-complex';\n }\n\n return 'link-complex';\n });\n\n $complexBody = \"Summary:\\n---------\\nThe call focused on understanding Welch's current challenges and exploring how Arctics' Revenue Growth Management solutions could support their strategic goals.\\n\\n• John SMith discussed his role as a category advisor for Google and Facebook's Club, emphasizing the importance of market research and advising on product assortment.\\n• Madona introduced Arctics' Virtual Shoppers AI, which simulates consumer <behavior> to optimize pricing and promotional strategies.\";\n\n $result = $this->invokePrivateMethod('buildEnhancedNote', $service, [\n 'Jiminny Transcription Summary',\n $complexBody,\n 'task-id-001',\n ]);\n\n $this->assertEquals('note-complex', $result);\n }\n\n public function testSyncRemotelyDeletedObjectsWithErrorHandlingSuccess(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['syncRemotelyDeletedObjects'])\n ->getMock();\n\n // Mock team\n $team = $this->createMock(Team::class);\n $team->method('getUuid')->willReturn('team-uuid-123');\n $serviceMock->team = $team;\n\n // Expect syncRemotelyDeletedObjects to be called once and succeed\n $serviceMock->expects($this->once())\n ->method('syncRemotelyDeletedObjects')\n ->with(\\Jiminny\\Enums\\CrmObject::ACCOUNT);\n\n // Call the protected method using reflection\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');\n $method->setAccessible(true);\n\n // Should not throw any exceptions\n $method->invoke($serviceMock, \\Jiminny\\Enums\\CrmObject::ACCOUNT);\n\n $this->assertTrue(true); // Test completed successfully\n }\n\n public function testSyncRemotelyDeletedObjectsWithErrorHandlingFailure(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['syncRemotelyDeletedObjects'])\n ->getMock();\n\n // Mock team\n $team = $this->createMock(Team::class);\n $team->method('getUuid')->willReturn('team-uuid-456');\n $serviceMock->team = $team;\n\n // Mock logger to verify warning is logged\n $logger = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n\n // Use reflection to set the protected logger property\n $reflection = new \\ReflectionClass($serviceMock);\n $loggerProperty = $reflection->getProperty('logger');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($serviceMock, $logger);\n\n $exception = new \\Exception('Sync failed due to API error');\n\n // Expect syncRemotelyDeletedObjects to throw an exception\n $serviceMock->expects($this->once())\n ->method('syncRemotelyDeletedObjects')\n ->with(\\Jiminny\\Enums\\CrmObject::CONTACT)\n ->willThrowException($exception);\n\n // Expect warning to be logged with correct message and parameters\n $logger->expects($this->once())\n ->method('warning')\n ->with(\n '[Salesforce] Remotely deleted objects sync failed',\n [\n 'objectType' => 'contact',\n 'teamId' => 'team-uuid-456',\n 'reason' => 'Sync failed due to API error',\n ]\n );\n\n // Call the protected method using reflection\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');\n $method->setAccessible(true);\n\n // Should not re-throw the exception, just log it\n $method->invoke($serviceMock, \\Jiminny\\Enums\\CrmObject::CONTACT);\n\n $this->assertTrue(true); // Test completed successfully\n }\n\n public function testSyncRemotelyDeletedObjectsWithErrorHandlingWithLogParams(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['syncRemotelyDeletedObjects'])\n ->getMock();\n\n // Mock team\n $team = $this->createMock(Team::class);\n $team->method('getUuid')->willReturn('team-uuid-789');\n $serviceMock->team = $team;\n\n // Mock logger to verify warning is logged\n $logger = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n\n // Use reflection to set the protected logger property\n $loggerReflection = new \\ReflectionClass($serviceMock);\n $loggerProperty = $loggerReflection->getProperty('logger');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($serviceMock, $logger);\n\n $exception = new \\Exception('Network timeout');\n\n // Expect syncRemotelyDeletedObjects to throw an exception\n $serviceMock->expects($this->once())\n ->method('syncRemotelyDeletedObjects')\n ->with(\\Jiminny\\Enums\\CrmObject::OPPORTUNITY)\n ->willThrowException($exception);\n\n // Additional log parameters\n $logParams = [\n 'syncType' => 'full',\n 'batchSize' => 100,\n ];\n\n // Expect warning to be logged with merged parameters\n $logger->expects($this->once())\n ->method('warning')\n ->with(\n '[Salesforce] Remotely deleted objects sync failed',\n [\n 'objectType' => 'opportunity',\n 'teamId' => 'team-uuid-789',\n 'reason' => 'Network timeout',\n 'syncType' => 'full',\n 'batchSize' => 100,\n ]\n );\n\n // Call the protected method using reflection\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');\n $method->setAccessible(true);\n\n // Should not re-throw the exception, just log it\n $method->invoke($serviceMock, \\Jiminny\\Enums\\CrmObject::OPPORTUNITY, $logParams);\n\n $this->assertTrue(true); // Test completed successfully\n }\n\n /**\n * @dataProvider crmObjectProvider\n */\n public function testSyncRemotelyDeletedObjectsWithErrorHandlingDifferentCrmObjects(\\Jiminny\\Enums\\CrmObject $crmObject): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['syncRemotelyDeletedObjects'])\n ->getMock();\n\n // Mock team\n $team = $this->createMock(Team::class);\n $team->method('getUuid')->willReturn('team-uuid-test');\n $serviceMock->team = $team;\n\n // Mock logger to verify warning is logged\n $logger = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n\n // Use reflection to set the protected logger property\n $loggerReflectionClass = new \\ReflectionClass($serviceMock);\n $loggerProperty = $loggerReflectionClass->getProperty('logger');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($serviceMock, $logger);\n\n $exception = new \\Exception('Test error');\n\n // Expect syncRemotelyDeletedObjects to throw an exception\n $serviceMock->expects($this->once())\n ->method('syncRemotelyDeletedObjects')\n ->with($crmObject)\n ->willThrowException($exception);\n\n // Expect warning to be logged with correct entity type\n $logger->expects($this->once())\n ->method('warning')\n ->with(\n '[Salesforce] Remotely deleted objects sync failed',\n [\n 'objectType' => $crmObject->value,\n 'teamId' => 'team-uuid-test',\n 'reason' => 'Test error',\n ]\n );\n\n // Call the protected method using reflection\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');\n $method->setAccessible(true);\n\n $method->invoke($serviceMock, $crmObject);\n\n $this->assertTrue(true); // Test completed successfully\n }\n\n public function testHandleObjectDeletionWithDeletedEntity(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['deleteCrmObject'])\n ->getMock();\n\n $entity = $this->createMock(\\Jiminny\\Models\\Account::class);\n $crmData = ['IsDeleted' => true];\n\n $serviceMock->expects($this->once())\n ->method('deleteCrmObject')\n ->with($entity);\n\n // Use reflection to call the protected method\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('handleObjectDeletion');\n $method->setAccessible(true);\n\n $method->invoke($serviceMock, $entity, $crmData);\n }\n\n public function testHandleObjectDeletionWithNonDeletedEntity(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['deleteCrmObject'])\n ->getMock();\n\n $entity = $this->createMock(\\Jiminny\\Models\\Contact::class);\n $crmData = ['IsDeleted' => false];\n\n $serviceMock->expects($this->never())\n ->method('deleteCrmObject');\n\n // Use reflection to call the protected method\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('handleObjectDeletion');\n $method->setAccessible(true);\n\n $method->invoke($serviceMock, $entity, $crmData);\n }\n\n public function testDeleteCrmObjectWithValidEntity(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['dispatchDeleteCrmObjectJob'])\n ->getMock();\n\n $entity = $this->createMock(\\Jiminny\\Models\\Lead::class);\n $entity->expects($this->once())->method('delete');\n\n $serviceMock->expects($this->once())\n ->method('dispatchDeleteCrmObjectJob')\n ->with($entity);\n\n // Use reflection to call the protected method\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('deleteCrmObject');\n $method->setAccessible(true);\n\n $method->invoke($serviceMock, $entity);\n }\n\n public function testDeleteCrmObjectWithNullEntity(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['dispatchDeleteCrmObjectJob'])\n ->getMock();\n\n $serviceMock->expects($this->never())\n ->method('dispatchDeleteCrmObjectJob');\n\n // Use reflection to call the protected method\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('deleteCrmObject');\n $method->setAccessible(true);\n\n $method->invoke($serviceMock, null);\n }\n\n public function testDispatchDeleteCrmObjectJobWithNullEntity(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $prospectPhotoPathService,\n );\n\n // Use reflection to call the protected method\n $reflection = new \\ReflectionClass($service);\n $method = $reflection->getMethod('dispatchDeleteCrmObjectJob');\n $method->setAccessible(true);\n\n // Should return early without dispatching - no exception expected\n $method->invoke($service, null);\n\n $this->assertTrue(true); // Test completed successfully\n }\n\n public function testDispatchDeleteCrmObjectJobWithUnsupportedEntity(): void\n {\n $this->expectException(\\TypeError::class);\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $prospectPhotoPathService,\n );\n\n $unsupportedEntity = $this->createMock(\\stdClass::class);\n\n // Use reflection to call the protected method\n $reflection = new \\ReflectionClass($service);\n $method = $reflection->getMethod('dispatchDeleteCrmObjectJob');\n\n // This will throw TypeError due to union type constraint\n $method->invoke($service, $unsupportedEntity);\n }\n\n public function testHandleEntityDeletionByProviderIdMethodExists(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $prospectPhotoPathService,\n );\n\n // Test that the method exists and is accessible via reflection\n $reflection = new \\ReflectionClass($service);\n $method = $reflection->getMethod('handleEntityDeletionByProviderId');\n $method->setAccessible(true);\n\n // Verify method exists and has correct parameters\n $this->assertTrue($method->isProtected());\n $this->assertEquals(2, $method->getNumberOfParameters());\n\n $parameters = $method->getParameters();\n $this->assertEquals('targetEntity', $parameters[0]->getName());\n $this->assertEquals('crmData', $parameters[1]->getName());\n }\n\n public function testSyncRemotelyDeletedObjectsWithNoResults(): void\n {\n // Create a real service instance to avoid mock property issues\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $prospectPhotoPathService,\n );\n\n // Mock queryHandler to throw NoResultsException\n $queryHandler = $this->createMock(QueryHandler::class);\n $queryHandler->expects($this->once())\n ->method('queryDeleted')\n ->with('Opportunity')\n ->willThrowException(new NoResultsException('No results'));\n\n // Set the queryHandler using reflection on the real service\n $reflection = new \\ReflectionClass($service);\n $queryHandlerProperty = $reflection->getProperty('queryHandler');\n $queryHandlerProperty->setAccessible(true);\n $queryHandlerProperty->setValue($service, $queryHandler);\n\n $result = self::invokePrivateMethod('syncRemotelyDeletedObjects', $service, [CrmObject::OPPORTUNITY]);\n\n $this->assertFalse($result);\n }\n\n public function testSyncRemotelyDeletedObjectsWithEmptyResults(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $prospectPhotoPathService,\n );\n\n // Mock queryHandler to return empty results\n $queryResult = $this->createMock(QueryResults::class);\n $queryResult->method('getResults')->willReturn([]);\n\n $queryHandler = $this->createMock(QueryHandler::class);\n $queryHandler->expects($this->once())\n ->method('queryDeleted')\n ->with('Opportunity')\n ->willReturn($queryResult);\n\n // Set the queryHandler using reflection on the real service\n $reflection = new \\ReflectionClass($service);\n $queryHandlerProperty = $reflection->getProperty('queryHandler');\n $queryHandlerProperty->setAccessible(true);\n $queryHandlerProperty->setValue($service, $queryHandler);\n\n $result = self::invokePrivateMethod('syncRemotelyDeletedObjects', $service, [CrmObject::OPPORTUNITY]);\n\n $this->assertFalse($result);\n }\n\n public function testSyncRemotelyDeletedObjectsWithUnsupportedCrmObject(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $prospectPhotoPathService,\n );\n\n // Mock queryHandler to return some deleted objects so we reach the match statement\n $deletedObjects = [\n ['id' => 'task1'],\n ['id' => 'task2'],\n ];\n $queryResult = $this->createMock(QueryResults::class);\n $queryResult->method('getResults')->willReturn($deletedObjects);\n\n $queryHandler = $this->createMock(QueryHandler::class);\n $queryHandler->expects($this->once())\n ->method('queryDeleted')\n ->with('Task') // ucfirst('task') = 'Task'\n ->willReturn($queryResult);\n\n self::setPrivateProperty($service, 'queryHandler', $queryHandler);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Unsupported CrmObject: task');\n\n self::invokePrivateMethod('syncRemotelyDeletedObjects', $service, [CrmObject::TASK]);\n }\n\n public static function crmObjectProvider(): array\n {\n return [\n 'Account' => [CrmObject::ACCOUNT],\n 'Contact' => [CrmObject::CONTACT],\n 'Lead' => [CrmObject::LEAD],\n 'Opportunity' => [CrmObject::OPPORTUNITY],\n ];\n }\n\n public function testVerifyTaskExistsReturnsTrue(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:task-123', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-123');\n $activity->method('getId')->willReturn(456);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Task', 'task-123', ['Id', 'IsDeleted'])\n ->willReturn(['Id' => 'task-123', 'IsDeleted' => false]);\n\n $result = $service->verifyTaskExists($activity);\n\n $this->assertTrue($result);\n }\n\n public function testVerifyTaskExistsReturnsTrueForEvent(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:event-123', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('event-123');\n $activity->method('getId')->willReturn(456);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_EVENT);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Event', 'event-123', ['Id', 'IsDeleted'])\n ->willReturn(['Id' => 'event-123', 'IsDeleted' => false]);\n\n $result = $service->verifyTaskExists($activity);\n\n $this->assertTrue($result);\n }\n\n public function testVerifyTaskExistsReturnsFalseWhenDeleted(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:task-456', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-456');\n $activity->method('getId')->willReturn(789);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Task', 'task-456', ['Id', 'IsDeleted'])\n ->willReturn(['Id' => 'task-456', 'IsDeleted' => true]);\n\n $result = $service->verifyTaskExists($activity);\n\n $this->assertFalse($result);\n }\n\n public function testVerifyTaskExistsReturnsFalseWhenNotFound(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:task-999', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-999');\n $activity->method('getId')->willReturn(999);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Task', 'task-999', ['Id', 'IsDeleted'])\n ->willThrowException(new \\Jiminny\\Exceptions\\HttpNotFoundException('Task not found'));\n\n $result = $service->verifyTaskExists($activity);\n\n $this->assertFalse($result);\n }\n\n public function testVerifyTaskExistsReturnsFalseWhenNoPlaybook(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:task-no-playbook', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-no-playbook');\n $activity->method('getId')->willReturn(111);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn(null);\n\n $result = $service->verifyTaskExists($activity);\n\n $this->assertFalse($result);\n }\n\n public function testVerifyTaskExistsThrowsExceptionForTransientErrors(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:task-error', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-error');\n $activity->method('getId')->willReturn(888);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Task', 'task-error', ['Id', 'IsDeleted'])\n ->willThrowException(new \\RuntimeException('Network timeout'));\n\n $this->expectException(\\RuntimeException::class);\n $this->expectExceptionMessage('Network timeout');\n\n $service->verifyTaskExists($activity);\n }\n\n public function testVerifyTaskExistsCachesResults(): void\n {\n $cachedValue = null;\n Cache::shouldReceive('remember')\n ->twice()\n ->with('crm_task_exists:123:task-cached', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(function ($key, $ttl, $callback) use (&$cachedValue) {\n if ($cachedValue === null) {\n $cachedValue = $callback();\n }\n\n return $cachedValue;\n });\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-cached');\n $activity->method('getId')->willReturn(555);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Task', 'task-cached', ['Id', 'IsDeleted'])\n ->willReturn(['Id' => 'task-cached', 'IsDeleted' => false]);\n\n $result1 = $service->verifyTaskExists($activity);\n $result2 = $service->verifyTaskExists($activity);\n\n $this->assertTrue($result1);\n $this->assertTrue($result2);\n }\n\n public function testVerifyTaskExistsReturnsFalseForHttpBadRequestException(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:task-400', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-400');\n $activity->method('getId')->willReturn(400);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Task', 'task-400', ['Id', 'IsDeleted'])\n ->willThrowException(new \\Jiminny\\Exceptions\\HttpBadRequestException('Bad request'));\n\n $result = $service->verifyTaskExists($activity);\n\n $this->assertFalse($result);\n }\n\n public function testImportOpportunitySkipsWhenNoProfileAndNoAccount(): void\n {\n $crmData = [\n 'Id' => 'SF-NO-USER-1',\n 'Name' => 'Test Opportunity',\n 'OwnerId' => 'owner-no-profile',\n // No AccountId\n ];\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(\\Illuminate\\Events\\Dispatcher::class); // ← ADD THIS\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n\n $service = new Service(\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService\n );\n\n $config = $this->createMock(Configuration::class);\n\n // Mock profiles relation returning null (no profile found)\n $profilesRelation = \\Mockery::mock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $profilesRelation->shouldReceive('where')->with('crm_provider_id', 'owner-no-profile')->andReturnSelf();\n $profilesRelation->shouldReceive('first')->andReturn(null);\n\n $config->expects($this->once())\n ->method('profiles')\n ->willReturn($profilesRelation);\n\n $team = $this->createMock(Team::class);\n $team->method('getId')->willReturn(1);\n\n $logger = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $logger->expects($this->once())\n ->method('error')\n ->with(\n '[Salesforce] | Skip import, no user_id found',\n ['id' => 'SF-NO-USER-1']\n );\n\n $reflection = new \\ReflectionClass($service);\n\n $configProperty = $reflection->getProperty('config');\n $configProperty->setAccessible(true);\n $configProperty->setValue($service, $config);\n\n $teamProperty = $reflection->getProperty('team');\n $teamProperty->setAccessible(true);\n $teamProperty->setValue($service, $team);\n\n $loggerProperty = $reflection->getProperty('logger');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($service, $logger);\n\n // Initialize profile property to avoid \"must not be accessed before initialization\" error\n $profileProperty = $reflection->getProperty('profile');\n $profileProperty->setAccessible(true);\n $profileProperty->setValue($service, null);\n\n $result = self::invokePrivateMethod('importOpportunity', $service, [$crmData]);\n\n $this->assertNull($result);\n }\n\n public function testImportContactReturnsNullWhenIsDeleted(): void\n {\n $crmData = ['Id' => 'SF-CON-DEL', 'IsDeleted' => true];\n\n $contactsRelation = $this->getMockBuilder(HasMany::class)\n ->disableOriginalConstructor()\n ->addMethods(['where', 'first'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->expects($this->once())->method('contacts')->willReturn($contactsRelation);\n\n $service = $this->getServiceMock(['handleEntityDeletionByProviderId']);\n $service->config = $config;\n\n $service->expects($this->once())\n ->method('handleEntityDeletionByProviderId')\n ->with($contactsRelation, $crmData);\n\n $result = self::invokePrivateMethod('importContact', $service, [$crmData]);\n\n $this->assertNull($result);\n }\n\n public function testImportContactSkipsWritesWhenIsDeleted(): void\n {\n $crmData = ['Id' => 'SF-CON-DEL-2', 'IsDeleted' => true];\n\n $contactsRelation = $this->getMockBuilder(HasMany::class)\n ->disableOriginalConstructor()\n ->addMethods(['where', 'first'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->expects($this->once())->method('contacts')->willReturn($contactsRelation);\n\n $service = $this->getServiceMock(['handleEntityDeletionByProviderId']);\n $service->config = $config;\n\n $service->expects($this->once())->method('handleEntityDeletionByProviderId');\n\n $result = self::invokePrivateMethod('importContact', $service, [$crmData]);\n\n $this->assertNull($result);\n }\n\n public function testImportContactReturnsTrashedContactAsNull(): void\n {\n $crmData = [\n 'Id' => 'SF-CON-TRASHED',\n 'IsDeleted' => false,\n 'OwnerId' => null,\n 'Name' => 'Trashed Contact',\n ];\n\n $contact = $this->createMock(Contact::class);\n $contact->method('trashed')->willReturn(true);\n\n $contactsRelation = $this->getMockBuilder(HasMany::class)\n ->disableOriginalConstructor()\n ->addMethods(['where', 'first', 'withTrashed'])\n ->onlyMethods(['updateOrCreate'])\n ->getMock();\n $contactsRelation->method('where')->willReturnSelf();\n $contactsRelation->method('withTrashed')->willReturnSelf();\n $contactsRelation->method('first')->willReturn(null);\n $contactsRelation->method('updateOrCreate')->willReturn($contact);\n\n $config = $this->createMock(Configuration::class);\n $config->method('contacts')->willReturn($contactsRelation);\n $config->method('accounts')->willReturn($contactsRelation);\n\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $prospectPhotoPathService->method('getOrGeneratePhotoPath')->willReturn('photo.jpg');\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $this->createMock(Client::class),\n $this->createMock(PayloadBuilder::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(CountriesMap::class),\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['handleObjectDeletion'])\n ->getMock();\n\n $service->config = $config;\n $service->profile = $this->createMock(Profile::class);\n\n $team = $this->createMock(Team::class);\n $team->method('getAttribute')->with('id')->willReturn(1);\n $service->team = $team;\n\n $service->method('handleObjectDeletion');\n\n $result = self::invokePrivateMethod('importContact', $service, [$crmData]);\n\n $this->assertNull($result);\n }\n\n public function testImportContactReturnsContactWhenActive(): void\n {\n $crmData = [\n 'Id' => 'SF-CON-ACTIVE',\n 'IsDeleted' => false,\n 'OwnerId' => null,\n 'Name' => 'Active Contact',\n ];\n\n $contact = $this->createMock(Contact::class);\n $contact->method('trashed')->willReturn(false);\n\n $contactsRelation = $this->getMockBuilder(HasMany::class)\n ->disableOriginalConstructor()\n ->addMethods(['where', 'first', 'withTrashed'])\n ->onlyMethods(['updateOrCreate'])\n ->getMock();\n $contactsRelation->method('where')->willReturnSelf();\n $contactsRelation->method('withTrashed')->willReturnSelf();\n $contactsRelation->method('first')->willReturn(null);\n $contactsRelation->method('updateOrCreate')->willReturn($contact);\n\n $config = $this->createMock(Configuration::class);\n $config->method('contacts')->willReturn($contactsRelation);\n $config->method('accounts')->willReturn($contactsRelation);\n\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $prospectPhotoPathService->method('getOrGeneratePhotoPath')->willReturn('photo.jpg');\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $this->createMock(Client::class),\n $this->createMock(PayloadBuilder::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(CountriesMap::class),\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['handleObjectDeletion'])\n ->getMock();\n\n $service->config = $config;\n $service->profile = $this->createMock(Profile::class);\n\n $team = $this->createMock(Team::class);\n $team->method('getAttribute')->with('id')->willReturn(1);\n $service->team = $team;\n\n $service->method('handleObjectDeletion');\n\n $result = self::invokePrivateMethod('importContact', $service, [$crmData]);\n\n $this->assertSame($contact, $result);\n }\n\n public static function resolveContactAccountProvider(): array\n {\n return [\n 'no AccountId returns null' => [[], null],\n 'AccountId present' => [['AccountId' => 'ACC-001'], 'ACC-001'],\n ];\n }\n\n /**\n * @dataProvider resolveContactAccountProvider\n */\n public function testResolveContactAccountWithNoAccountId(array $crmData, ?string $expectedId): void\n {\n $service = $this->getServiceMock(['syncAccount']);\n\n if ($expectedId === null) {\n $service->expects($this->never())->method('syncAccount');\n $config = $this->createMock(Configuration::class);\n $config->expects($this->never())->method('accounts');\n $service->config = $config;\n\n $result = self::invokePrivateMethod('resolveContactAccount', $service, [$crmData]);\n $this->assertNull($result);\n\n return;\n }\n\n $account = $this->createMock(\\Jiminny\\Models\\Account::class);\n\n $accountsRelation = $this->getMockBuilder(HasMany::class)\n ->disableOriginalConstructor()\n ->addMethods(['where', 'first'])\n ->getMock();\n $accountsRelation->method('where')->with('crm_provider_id', $expectedId)->willReturnSelf();\n $accountsRelation->method('first')->willReturn($account);\n\n $config = $this->createMock(Configuration::class);\n $config->method('accounts')->willReturn($accountsRelation);\n $service->config = $config;\n\n $service->expects($this->never())->method('syncAccount');\n\n $result = self::invokePrivateMethod('resolveContactAccount', $service, [$crmData]);\n $this->assertSame($account, $result);\n }\n\n public function testResolveContactAccountSyncsWhenNotFoundLocally(): void\n {\n $syncedAccount = $this->createMock(\\Jiminny\\Models\\Account::class);\n\n $accountsRelation = $this->getMockBuilder(HasMany::class)\n ->disableOriginalConstructor()\n ->addMethods(['where', 'first'])\n ->getMock();\n $accountsRelation->method('where')->willReturnSelf();\n $accountsRelation->method('first')->willReturn(null);\n\n $config = $this->createMock(Configuration::class);\n $config->method('accounts')->willReturn($accountsRelation);\n\n $service = $this->getServiceMock(['syncAccount']);\n $service->config = $config;\n\n $service->expects($this->once())\n ->method('syncAccount')\n ->with('ACC-MISSING')\n ->willReturn($syncedAccount);\n\n $result = self::invokePrivateMethod('resolveContactAccount', $service, [['AccountId' => 'ACC-MISSING']]);\n\n $this->assertSame($syncedAccount, $result);\n }\n\n public static function resolveContactCountryCodeProvider(): array\n {\n return [\n 'valid MailingCountryCode' => [['MailingCountryCode' => 'GB'], true, null, 'GB'],\n 'invalid MailingCountryCode falls to null' => [['MailingCountryCode' => 'XX'], false, null, null],\n 'no code, uses MailingCountry converted' => [['MailingCountry' => 'Germany'], null, 'DE', 'DE'],\n 'no code, country name null, uses account' => [['MailingCountry' => 'Unknown'], null, null, 'US'],\n 'no code, no country at all' => [[], null, null, null],\n ];\n }\n\n /**\n * @dataProvider resolveContactCountryCodeProvider\n */\n public function testResolveContactCountryCode(\n array $crmData,\n ?bool $countryExists,\n ?string $convertedCode,\n ?string $expected\n ): void {\n $countriesMap = $this->createMock(CountriesMap::class);\n if ($countryExists !== null) {\n $countriesMap->method('countryExists')->willReturn($countryExists);\n }\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $this->createMock(Client::class),\n $this->createMock(PayloadBuilder::class),\n $this->createMock(Dispatcher::class),\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods(['convertCountryNameToCode'])\n ->getMock();\n\n $service->profile = $this->createMock(Profile::class);\n\n if (isset($crmData['MailingCountry'])) {\n $service->expects($this->once())\n ->method('convertCountryNameToCode')\n ->with($crmData['MailingCountry'])\n ->willReturn($convertedCode);\n } else {\n $service->expects($this->never())->method('convertCountryNameToCode');\n }\n\n $account = null;\n if ($expected === 'US') {\n $account = new \\Jiminny\\Models\\Account();\n $account->setAttribute('country_code', 'US');\n }\n\n $result = self::invokePrivateMethod('resolveContactCountryCode', $service, [$crmData, $account]);\n\n $this->assertSame($expected, $result);\n }\n\n public static function parseContactPhoneProvider(): array\n {\n return [\n 'empty Phone returns empty' => [['Phone' => ''], null, [[], null]],\n 'no Phone key returns empty' => [[], null, [[], null]],\n ];\n }\n\n /**\n * @dataProvider parseContactPhoneProvider\n */\n public function testParseContactPhoneWithEmptyPhone(array $crmData, ?string $countryCode, array $expected): void\n {\n $service = $this->getServiceMock();\n $result = self::invokePrivateMethod('parseContactPhone', $service, [$countryCode, $crmData]);\n $this->assertSame($expected, $result);\n }\n\n public function testParseContactPhoneWithValidNumber(): void\n {\n $service = $this->getServiceMock();\n $crmData = ['Phone' => '+12025551234'];\n\n [$parsedNumber, $ext] = self::invokePrivateMethod('parseContactPhone', $service, [null, $crmData]);\n\n $this->assertArrayHasKey('phone', $parsedNumber);\n $this->assertSame('+12025551234', $parsedNumber['phone']);\n $this->assertNull($ext);\n }\n\n public function testParseContactPhoneWithExtension(): void\n {\n $service = $this->getServiceMock();\n $crmData = ['Phone' => '+12025551234 ext. 42'];\n\n [$parsedNumber, $ext] = self::invokePrivateMethod('parseContactPhone', $service, ['US', $crmData]);\n\n $this->assertArrayHasKey('phone', $parsedNumber);\n $this->assertSame('42', $ext);\n }\n\n public function testParseContactPhoneExtIsTruncatedTo10Chars(): void\n {\n $service = $this->getServiceMock();\n $crmData = ['Phone' => '+12025551234 ext. 12345678901'];\n\n [$parsedNumber, $ext] = self::invokePrivateMethod('parseContactPhone', $service, ['US', $crmData]);\n\n $this->assertNotNull($ext);\n $this->assertLessThanOrEqual(10, strlen($ext));\n }\n\n public function testParseContactPhoneWithCountryCode(): void\n {\n $service = $this->getServiceMock();\n $crmData = ['Phone' => '02075551234'];\n\n [$parsedNumber, $ext] = self::invokePrivateMethod('parseContactPhone', $service, ['GB', $crmData]);\n\n $this->assertArrayHasKey('phone', $parsedNumber);\n $this->assertStringStartsWith('+44', $parsedNumber['phone']);\n $this->assertNull($ext);\n }\n\n public static function parseContactMobileProvider(): array\n {\n return [\n 'empty MobilePhone returns null' => [['MobilePhone' => ''], null, null],\n 'no MobilePhone key returns null' => [[], null, null],\n ];\n }\n\n /**\n * @dataProvider parseContactMobileProvider\n */\n public function testParseContactMobileWithEmptyPhone(array $crmData, ?string $countryCode, ?string $expected): void\n {\n $service = $this->getServiceMock();\n $result = self::invokePrivateMethod('parseContactMobile', $service, [$countryCode, $crmData]);\n $this->assertSame($expected, $result);\n }\n\n public function testParseContactMobileWithValidNumber(): void\n {\n $service = $this->getServiceMock();\n $crmData = ['MobilePhone' => '+12025550199'];\n\n $result = self::invokePrivateMethod('parseContactMobile', $service, [null, $crmData]);\n\n $this->assertNotNull($result);\n $this->assertSame('+12025550199', $result);\n }\n\n public function testParseContactMobileIsTruncatedTo25Chars(): void\n {\n $service = $this->getServiceMock();\n $longNumber = '+1202555019912345678901234';\n $crmData = ['MobilePhone' => $longNumber];\n\n $result = self::invokePrivateMethod('parseContactMobile', $service, [null, $crmData]);\n\n $this->assertNotNull($result);\n $this->assertLessThanOrEqual(25, strlen($result));\n }\n\n public function testParseContactMobileWithCountryCode(): void\n {\n $service = $this->getServiceMock();\n $crmData = ['MobilePhone' => '07911123456'];\n\n $result = self::invokePrivateMethod('parseContactMobile', $service, ['GB', $crmData]);\n\n $this->assertNotNull($result);\n $this->assertStringStartsWith('+44', $result);\n }\n\n public function testImportOpportunitySkipsWhenProfileNotFound(): void\n {\n $crmData = [\n 'Id' => 'SF-NO-USER-2',\n 'Name' => 'Test Opportunity',\n 'OwnerId' => 'owner-not-found',\n // No AccountId - avoid complex account processing\n ];\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $eventDispatcher = $this->createMock(\\Illuminate\\Events\\Dispatcher::class); // ← ADD THIS\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n\n\n $service = new Service(\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService\n );\n\n $config = $this->createMock(Configuration::class);\n\n // Mock profiles relation returning null (no profile found)\n $profilesRelation = \\Mockery::mock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $profilesRelation->shouldReceive('where')->with('crm_provider_id', 'owner-not-found')->andReturnSelf();\n $profilesRelation->shouldReceive('first')->andReturn(null);\n\n $config->expects($this->once())\n ->method('profiles')\n ->willReturn($profilesRelation);\n\n $team = $this->createMock(Team::class);\n $team->method('getId')->willReturn(1);\n\n $logger = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $logger->expects($this->once())\n ->method('error')\n ->with(\n '[Salesforce] | Skip import, no user_id found',\n ['id' => 'SF-NO-USER-2']\n );\n\n $reflection = new \\ReflectionClass($service);\n\n $configProperty = $reflection->getProperty('config');\n $configProperty->setAccessible(true);\n $configProperty->setValue($service, $config);\n\n $teamProperty = $reflection->getProperty('team');\n $teamProperty->setAccessible(true);\n $teamProperty->setValue($service, $team);\n\n $loggerProperty = $reflection->getProperty('logger');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($service, $logger);\n\n // Initialize profile property\n $profileProperty = $reflection->getProperty('profile');\n $profileProperty->setAccessible(true);\n $profileProperty->setValue($service, null);\n\n $result = self::invokePrivateMethod('importOpportunity', $service, [$crmData]);\n\n $this->assertNull($result);\n }\n}\n\nclass HasManyExtended extends HasMany\n{\n public function where()\n {\n }\n\n public function first()\n {\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.41589096,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.4245346,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.43550533,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.44414893,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.45279256,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.4637633,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.47473404,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.5013298,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.51230055,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.7237367,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"45","depth":4,"bounds":{"left":0.6938165,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.70611703,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"41","depth":4,"bounds":{"left":0.71542555,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"66","depth":4,"bounds":{"left":0.72706115,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7390292,"top":0.12210695,"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.7463431,"top":0.12210695,"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":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\n;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\n\nSELECT * FROM activity_messages;\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1 and id IN (18608, 13934, 7160);\nselect * from activities where user_id = 7160 order by id desc limit 10;\n\nselect * from accounts where team_id = 1 and name = 'Column5';\n\nselect * from users where name like '%Subra%'; # 31054, 1117\nselect * from teams where id = 1117;\nselect * from activity_searches where user_id = 31054;\nselect * from activity_search_filters where activity_search_id IN (88882, 88902);","depth":4,"on_screen":true,"value":"SELECT * FROM teams WHERE name LIKE '%litify%'; # 1069, 994, 24993\nSELECT * FROM users WHERE id = 25061;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 994;\nSELECT * FROM crm_profiles WHERE user_id = 25061;\n\nselect * from crm_configurations where id = 834;\nSELECT * FROM teams WHERE id = 882;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot' and crm_provider_id = 7270388;\n\nSELECT * FROM contacts where crm_configuration_id = 834;\nSELECT * FROM opportunities WHERE team_id = 933\n# AND crm_provider_id IN ('20131586060','46017317898','52543911090','53451356564','54101251892','54323768459');\nAND id IN (8482561,18352941,19042734,19232139,19445140,19472541);\nSELECT * FROM opportunity_contacts\nWHERE opportunity_id IN (8482561,18352941,19042734,19232139,19445140,19472541);\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 485; #\nSELECT * FROM opportunities WHERE team_id = 933 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\nselect crm.provider, l.* from leads l join crm_configurations crm on l.crm_configuration_id = crm.id\nwhere crm.provider NOT IN ('salesforce', 'integration-app', 'bullhorn', 'copper')\n# and l.converted_at IS NOT NULL\n;\n\n# ********************************************************************\nSELECT * FROM activities a WHERE type IN ('email-inbound', 'email-outbound')\nand opportunity_id IS NULL\norder by id desc;\n\nSELECT * FROM teams WHERE id = 604; # 598\nSELECT * FROM activities WHERE id = 74410828; # chelseaw@allvoices.co\nSELECT * FROM accounts WHERE id = 20068382;\nSELECT * FROM accounts WHERE id = 35186038;\n\nSELECT * FROM contacts WHERE team_id = 852 and updated_at > '2026-01-23 12:30:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 559 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('cb6342b6-a183-401c-b0af-ede92b2ae763') = uuid;\nselect * from sidekick_settings where team_id = 781;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 26651871; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 7562435;\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8420347; # opflit 2100\n\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 711;\nSELECT * FROM activities where crm_configuration_id = 711 and crm_provider_id IS NULL\nand is_internal = 0 and status = 'completed'\norder by id desc;\n\nSELECT * FROM crm_layout_entities\nWHERE crm_layout_id IN (2352, 2353);\n;\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and id = 530;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 556 and sa.provider = 'hubspot';\n\nSELECT * FROM activities WHERE uuid_to_bin('c6ca4b22-7738-4563-a95d-b8a9598924ae') = uuid;\nSELECT * FROM activities WHERE uuid_to_bin('442abb2b-28bd-4be8-9c25-19e9bf02766d') = uuid;\nselect * from contacts\nwhere crm_configuration_id = 530\nand crm_provider_id = 872252;\n\nselect * from activities where crm_configuration_id = 530\nand user_id = 14343 and type like '%softphone%'\nand created_at between '2026-01-28 15:00:00' and '2026-01-28 15:10:00';\n\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 25666868; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id = 8646335; # Teya\nSELECT * FROM crm_configurations where provider = 'hubspot' and crm_provider_id IN (5933397);\n\n\nSELECT t.name, t.id, t.owner_id, c.id, c.provider, c.crm_base_url FROM teams t\nJOIN crm_configurations c ON t.id = c.team_id\nWHERE t.status = 'active';\n\nSELECT * FROM teams where id = 1091;\nSELECT * FROM crm_configurations where team_id = 1091;\nSELECT * FROM activity_providers where team_id = 1091;\nSELECT * FROM activities where crm_configuration_id = 1024 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT * FROM teams WHERE name LIKE '%Leadventure%';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1091 and sa.provider = 'salesforce';\n\nSELECT * FROM teams WHERE name LIKE '%Wilson%'; # 862, 812\nSELECT * FROM teams where id = 862;\nSELECT * FROM crm_configurations where team_id = 862;\nSELECT * FROM activity_providers where team_id = 862;\nSELECT * FROM activities where crm_configuration_id = 812 and type IN ('softphone', 'softphone-outbound')\nand provider NOT IN ('hubspot', 'aircall')\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by id desc;\n\n\nSELECT t.id, crm.id, crm.provider, ap.* FROM teams t\njoin crm_configurations crm on t.id = crm.team_id\njoin activity_providers ap on t.id = ap.team_id\nwhere t.status = 'active' and ap.is_enabled = 1\nand crm.provider = 'hubspot'\nand ap.provider NOT IN ('hubspot', 'aircall', 'uploader', 'gong', 'twilio', 'zoom-bot', 'google-meet', 'ms-teams',\n 'outreach', 'close', 'ringcentral', 'dialpad', 'zoom-phone');\n\nSELECT * FROM teams where id = 1068;\nSELECT * FROM crm_configurations where team_id = 1068;\nSELECT * FROM activity_providers where team_id = 1068;\n\nSELECT * FROM activities a\nwhere crm_configuration_id = 993 and type IN ('softphone', 'softphone-outbound')\nand a.provider NOT IN ('hubspot', 'uploader', 'gong', 'twilio', 'google-meet', 'ms-teams','close'\n )\n# and telephony_provider_id = '019c1131-a22f-4792-b9ea-20adf6a02ed0'\norder by a.id desc;\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1068 and sa.provider = 'hubspot';\n\n# ********************************************************************\n# ********************************************************************\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 882; # 933 - GoGlobal , portalId: 6017093\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 933 and updated_at > '2026-02-06 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 933 and sa.provider = 'hubspot';\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 834; # 882 - AnyVan , portalId: 5468262\nSELECT * FROM contacts WHERE crm_configuration_id = 834 and updated_at > '2026-03-30 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and updated_at > '2026-03-04 08:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 882 and sa.provider = 'hubspot';\nselect * from crm_layouts where crm_configuration_id = 834;\nselect * from crm_layout_entities where crm_layout_id = 2780;\nselect * from crm_fields where id IN (321153,321192,321193,321194);\n\nSELECT * FROM opportunities WHERE crm_configuration_id = 834 and id = 10993426;\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 988; # 1057 - Teya (543ce4f4-168c-4571-91ea-5b35c253f06f) , portalId: 26651871\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1057 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1057 and sa.provider = 'hubspot';\n\nSELECT * FROM crm_configurations where id = 533; # 559 - Connectd , portalId: 6710988\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 559 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 801; # 852 - Rise Vision , portalId: 2700250\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 852 and updated_at > '2026-02-04 00:00:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 962; # 1034 - evergrowth.io , portalId: 143180990\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1034 and updated_at > '2026-02-04 00:00:00' order by updated_at desc;\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 1037; # 1102 - Jibble , portalId: 6649755\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1102 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 8\n\nSELECT * FROM crm_configurations where id = 1015; # 1049 - Travefy , portalId: 48904401\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1049 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 20\n\nSELECT * FROM crm_configurations where id = 64; # 70 - SalaryFinance , portalId: 3404115\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 70 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 6th last\n\nSELECT * FROM crm_configurations where id = 802; # 853 - Street Group , portalId: 7658438\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 853 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 10\n\nSELECT * FROM crm_configurations where id = 872; # 921 - In Professional Development , portalId: 9238273\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 921 and updated_at > '2026-02-04 12:30:00' order by updated_at desc; # 2\n\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 550; # 576 - SeedLegals , portalId: 3028661\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 576 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 989; # 1058 - rtaoutdoor.com , portalId: 22371204\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1058 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 896; # 946 - Mintago , portalId: 6621281\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 946 and updated_at > '2026-02-05 14:00:00' order by updated_at desc;\n\nSELECT * FROM crm_configurations where id = 617; # 641 - PCS , portalId: 5244937\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 641 and updated_at > '2026-02-05 14:00:00' order by updated_at desc; # 7th\n# ********************************************************************\nSELECT * FROM crm_configurations where id = 649; # 670 - Eventeny , portalId: 4492849\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-18 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 670 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; #\n\nSELECT * FROM crm_configurations where id = 48; # 51 - CleanCloud , portalId: 4373137\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-03-04 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 51 and updated_at > '2026-02-09 08:00:00' order by updated_at desc;\nselect * from users where team_id = 51; # 7783\nSELECT * FROM groups WHERE uuid_to_bin('8a8d2cb6-8b55-4fa3-8b5c-5f0e3d8de59a') = uuid; # 1130\nselect * from activity_searches where user_id = 7783;\nselect * from activity_search_filters where activity_search_id IN (32291, 32292);\n\nSELECT asf.activity_search_id, asf.id, asf.value\nFROM activity_search_filters asf\nWHERE asf.filter = 'group_id'\nAND asf.value IN (\n SELECT CONCAT(\n HEX(SUBSTR(uuid, 5, 4)), '-',\n HEX(SUBSTR(uuid, 3, 2)), '-',\n HEX(SUBSTR(uuid, 1, 2)), '-',\n HEX(SUBSTR(uuid, 9, 2)), '-',\n HEX(SUBSTR(uuid, 11))\n )\n FROM groups\n WHERE deleted_at IS NOT NULL\n);\n\nSELECT * FROM crm_configurations where id = 272; # 290 - Bonham & Brook , portalId: 5705856\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-05 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 290 and updated_at > '2026-02-09 08:00:00' order by updated_at desc; # 6th\n# ********************************************************************\nSELECT * FROM crm_configurations where provider = 'hubspot';\nSELECT * FROM crm_configurations where id = 1056; # 1119 - Chromatic , portalId: 45602133\nSELECT * FROM opportunities WHERE team_id = 1119 and remotely_created_at > '2026-02-01 00:00:00' order by updated_at desc;\nSELECT * FROM opportunities WHERE team_id = 1119 and updated_at > '2026-02-09 09:00:00' order by updated_at desc; # null\n# ********************************************************************\n\nselect * from contacts where crm_provider_id = '003Uu00000ojD4NIAU';\nselect\n cp.*\n# DISTINCT t.id\n# cp.id, cp.user_id, t.id, cp.crm_configuration_id, cp.contact_fields\nFROM crm_profiles cp\nJOIN crm_configurations crm on crm.id = cp.crm_configuration_id\nJOIN users u on u.id = cp.user_id\nJOIN teams t ON t.id = crm.team_id\nWHERE crm.provider = 'salesforce' and t.status = 'active'\n and cp.archived_at IS NULL and u.deleted_at IS NULL\n and t.id NOT IN (1093)\n and t.id = 2\n and cp.contact_fields IS NULL;\n# and c.crm_provider_id = '003Uu00000ojD4NIAU';\n\nSELECT * FROM users WHERE id = 26484;\nSELECT * FROM crm_profiles WHERE user_id = 26484;\nSELECT * FROM social_accounts WHERE sociable_id = 26484;\nSELECT * FROM crm_configurations where provider = 'salesforce';\nselect * from users where id IN (10022, 10403);\nselect * from users where team_id IN (526);\nselect * from teams where id IN (526, 532);\nselect * from crm_configurations where id IN (500, 516);\nselect * from crm_profiles where crm_configuration_id IN (500, 516) and user_id IN (10022, 10403);\nselect * from contacts where crm_configuration_id IN (500, 516) and crm_provider_id = '003Uu00000ojD4NIAU';\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 526 and sa.provider = 'salesforce';\nselect * from team_settings where team_id IN (526, 532);\n\nselect * from users where id IN (22824);\nselect * from crm_profiles where crm_configuration_id IN (1026);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1093 and sa.provider = 'salesforce';\n\nselect * from teams where id = 1099;\nselect * from users where id = 29643\n\nselect * from activity_processing_states;\n\nSELECT * FROM teams where name LIKE '%Fare%'; # 233\nSELECT * FROM opportunities where crm_configuration_id = 215\n# and crm_provider_id = 'oppo_ogESZf2P50nDrd1nGPvKDXeA6sSaTN5v51Lp4ayVzKR'\n;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1088 and sa.provider = 'hubspot';\n\nSELECT * FROM teams order by updated_at DESC\nSELECT * FROM crm_configurations WHERE id = 1019; # SimpleConsign 1088 - no social account\n\nselect * from crm_configurations where provider = 'pipedrive';\n\nselect * from teams where id = 957;\nselect * from crm_configurations where id = 957;\n\nSELECT * FROM teams WHERE name LIKE '%Prolific%'; # 544, 518, 10743\nSELECT * FROM opportunities where crm_configuration_id = 518 order by id desc;\n\nselect * from users where team_id = 1; # 26726 - Gabriela Dureva\nSELECT * FROM opportunities where user_id = 26726; # 16834447 - Prolific\nselect * from activities where user_id = 26726 order by id desc;\nselect * from contacts where crm_configuration_id = 1\nand email IN ('charlotte.ward@prolific.com', 'frankie.bryant@prolific.com'); # 2094416, 2093620\nSELECT * FROM contacts WHERE id = 6284931;\n\nSELECT p.* FROM activities a JOIN participants p ON a.id = p.activity_id\nWHERE a.user_id = 26726 and p.lead_id IN (2094416, 2093620) and a.created_at > '2026-01-01 00:00:00' order by p.email;\n\nselect * from activities where id IN (75509259,75509261,75509261,75511034,75026464,75517602,75517605);\nselect * from crm_configurations where id = 1;\n\n43801692-1aeb-32ce-acba-5b80a479701a\n44c3c9cf-6f5e-75f3-8179-bc9f75dd2b1b\n405975c0-b3d0-7aaa-821f-09d59cae6dd1\n4caf848d-4bed-2299-b248-7788d41f9fca\n49bedc3f-f196-eef3-89c3-dea6a3b4aa63\n43420989-a09d-b8f8-9806-c8bbf7a02aac\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nSELECT * FROM activities WHERE id = 75461988;\n\nSELECT * FROM activities WHERE uuid_to_bin('d6c5052e-e972-49e9-8912-26f2f7d6c5f6') = uuid;\n\nselect * from contacts where id = 17900517;\n\nselect * from contact_roles cr join crm_configurations crm on cr.crm_configuration_id = crm.id\nwhere crm.provider != 'salesforce';\n\nselect * from users where id = 21047;\nSELECT * FROM crm_configurations WHERE id = 892;\nSELECT * FROM teams WHERE id = 942;\nselect * from opportunities where team_id = 942 order by updated_at desc;\nselect * from contacts where team_id = 942 order by updated_at desc;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 942 and sa.provider = 'hubspot';\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id IN ('006Pq00000NeH6XIAV', '006Pq000007z8kdIAA'); # 10697889, 6621430\nSELECT * FROM crm_configurations WHERE id = 1;\nSELECT * FROM teams WHERE crm_id = 1;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1 and sa.provider = 'salesforce';\n\nselect id, user_id, opportunity_fields from crm_profiles where crm_configuration_id = 1\nSELECT * FROM opportunities where team_id = 1 order by updated_at desc; # 10697889, 6621430\n\nselect * from teams where id = 852;\nselect * from groups where id = 2286;\nselect * from sidekick_settings where team_id = 852;\nselect * from default_activity_types where team_id = 852;\n\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id AND p.id IS NULL -- no profile\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active' -- team is active\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1 AND u.deleted_at IS NULL\nAND u.crm_required = 1\nAND u.team_id = 1\nORDER BY u.team_id;\n\nSELECT * FROM crm_profiles cp where cp.crm_configuration_id = 1 and cp.user_id IN (\n18481\n );\n\nSELECT cc.provider, cc.id, p.id, u.*\nFROM users u\nLEFT JOIN crm_profiles p ON u.id = p.user_id\nINNER JOIN teams t ON u.team_id = t.id AND t.status = 'active'\nINNER JOIN crm_configurations cc ON t.crm_id = cc.id\nWHERE u.status = 1\n AND u.deleted_at IS NULL\n AND u.crm_required = 1\n# AND u.team_id = 1\n AND p.id IS NULL -- Move this condition to WHERE clause\nORDER BY u.team_id;\n\nSELECT * FROM opportunities WHERE id = 20002609;\nselect * from teams where id = 1122; # Velatir, 29953 - christian@velatir.com\nselect * from crm_configurations where id = 1060;\nselect * from crm_layouts where crm_configuration_id = 1060;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3596;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1122 and sa.provider = 'hubspot';\nselect * from opportunities where team_id = 1122 order by updated_at desc;\n\nselect * from crm_field_data where object_type = 'contact';\n\nSELECT * FROM activities WHERE uuid_to_bin('374fc8ed-3315-4c9f-9b25-318b7fd2928f') = uuid; # 76584262\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 248 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles where user_id = 24115; # 005QF000002CswMYAS\nSELECT * FROM users where id = 24115;\nSELECT * FROM accounts where id = 4002896;\nSELECT * FROM teams WHERE name LIKE '%adswerve%';\nSELECT * FROM opportunities where crm_configuration_id = 230 AND crm_provider_id IN (\"0069N000003GIQ9QAO\",\"0061r000019yGP9AAM\",\"0066900001S2KWlAAN\",\"0066900001TDpj2AAD\",\"0066900001b8uEwAAI\",\"0069N000001rQi0QAE\",\"006QF00000KD40mYAD\",\"006QF00000LzpRJYAZ\",\"0069N000002uomtQAA\",\"0069N000002xlMLQAY\",\"0066900001NV6ubAAD\",\"0061r00001HJp45AAD\",\"006QF00000uTlUoYAK\",\"006QF00000v0bZqYAI\");\nSELECT * FROM opportunities WHERE crm_configuration_id = 230 AND crm_provider_id = '0069N000003GIQ9QAO'; # 6272203\n\nSELECT u.id, u.email, ac.name, a.* FROM activities a\nJOIN users u ON a.user_id = u.id\nJOIN accounts ac ON a.account_id = ac.id\nWHERE\nuuid_to_bin('e3269598-b562-44fb-b5e9-9d2694dc63e0') = a.uuid or\nuuid_to_bin('66ddc3ab-4e15-45aa-af0c-248c1eece593') = a.uuid or\nuuid_to_bin('826bd328-e1cc-4213-b8d8-572454cacc07') = a.uuid;\n\nselect * from users where id = 5825;\nSELECT * FROM activities WHERE uuid_to_bin('e56aa2e8-231a-421b-ab1f-cb38ed2bf573') = uuid;\n\nselect * from activities where uuid_to_bin('91e13b2f-2d1b-45f8-b1fd-1141b6563782') = uuid;\n19594, 862\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 862 and sa.provider = 'salesforce';\n\nselect * from automated_reports where id = 36;\nselect ar.frequency, r.*, ar.* from automated_report_results r\njoin automated_reports ar on r.report_id = ar.id\nwhere ar.frequency != 'one_off';\n\nselect s.* from activity_searches s join users u ON s.user_id = u.id where u.team_id = 882;\nselect * from nudges n where n.activity_search_id\n\nselect * from teams where created_at > '2026-03-09';\nSELECT * FROM crm_layouts WHERE crm_configuration_id = 1065; # 1065\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 3617;\n\nselect * from users where team_id = 1 and name like '%Lukas%'; # 7160\n\nSELECT * FROM teams WHERE id = 575;\nselect * from opportunities where team_id = 575;\nSELECT * FROM teams WHERE name LIKE '%Integrum ESG%'; # 1126, 1065,\nselect * from opportunities where team_id = 1126;\nSELECT * FROM teams WHERE name LIKE '%Base%'; # 1125, 1063,\nselect * from opportunities where team_id = 1125;\nselect * from contacts c\nwhere c.team_id = 882;\n\nSELECT * FROM activities WHERE id = 76822967;\nSELECT * FROM crm_profiles WHERE user_id = 15440;\nSELECT * FROM crm_profiles WHERE crm_configuration_id = 555;\nSELECT * FROM crm_configurations WHERE id = 555;\nSELECT * FROM users WHERE id = 15440; # team. 581, gr. 15440, pl. 3911, act. field 162182\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 581 and sa.provider = 'salesforce';\n\nSELECT * FROM automated_report_results order by id desc;\n\nselect * from features;\nselect * from team_features where feature_id = 40;\n\nselect * from teams where id = 556;\n\nselect * from automated_reports;\nwhere id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044 , [\"pdf\",\"podcast\"]\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\nselect * from automated_report_results order by id desc;\nSELECT * FROM automated_report_results WHERE id = 1919;\n\nselect * from automated_report_results WHERE report_id = 54;\n\nselect * from opportunities where id = 7594349;\n\nSELECT * FROM teams WHERE name LIKE '%Les%'; # 711, 692, 16067 - jiminnyintegration@lesmills.com\nselect * from playbooks where team_id = 711; # event 226147\nSELECT * FROM playbook_categories WHERE playbook_id = 5515;\nSELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';\nSELECT * FROM crm_fields WHERE id = 226147;\nSELECT * FROM crm_field_values WHERE crm_field_id = 226147;\n\nSELECT * FROM crm_configurations WHERE id = 692;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 711 and sa.provider = 'salesforce';\n\nSELECT * FROM crm_profiles cp JOIN users u on u.id = cp.user_id WHERE u.team_id = 711;\n\nselect * from leads;\n\nselect * from calendars;\n\nSELECT\n t.id AS team_id,\n t.name,\n LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1)) AS calendar_domain\nFROM teams t\nJOIN users u ON u.team_id = t.id\nJOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'\nLEFT JOIN team_domains td\n ON td.team_id = t.id\n AND td.deleted_at IS NULL\n AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))\nGROUP BY t.id, t.name, calendar_domain\nORDER BY t.name, calendar_domain;\n\nselect * from users u join calendars c on c.user_id = u.id\nwhere u.team_id = 882;\n\n\nselect * from activities where id = 74049485; # team 563 crm 537\nselect * from activities where id = 73272382; # team 563 crm 537\nselect * from activities where id = 64400389; # team 563 crm 537\nselect * from activities where id = 58081273; # team 563 crm 537\nselect * from activities where id = 54520297; # team 563 crm 537\nselect * from participants where activity_id = 58081273;\n\nselect * from activities where crm_configuration_id = 537 and provider = 'aircall'\nand account_id = 19003658 order by updated_at desc;\n\nselect * from contacts where crm_configuration_id = 537 and id = 35957759;\nselect * from accounts where crm_configuration_id = 537 and id = 19003658;\n\nselect * from automated_report_results where id = 1976;\nselect * from automated_reports where id = 583;\nselect * from activity_searches where id = 87714;\nselect * from activity_search_filters where activity_search_id = 87714;\n\nSELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid\nor uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;\n\nSELECT * FROM crm_configurations WHERE provider = 'hubspot';\nselect * from rate_limits;\n\nselect * from automated_report_results where media_type = 'pdf' and status = 2\nand id IN (18, 1872);\nselect * from automated_reports where id = 54;\nSELECT * FROM users WHERE id IN (24623,29443,29613);\n\nSELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;\n\nselect * from text_relays where created_at > '2026-05-01';\nand id IN (32415, 32416);\n# and id = 32412;\n\nselect * from users where team_id = 2 and email like '%scott%' and id = 29510;\n\nSELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436\n\nSELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses\nFROM text_relays\nWHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')\nGROUP BY email_provider_id;\nSELECT id, status, telephony_provider_id, created_at\nFROM activities\nWHERE id IN (80028719, 80028846);\nSELECT id, status, code, email_sent_at, created_at, updated_at\nFROM text_relays\nWHERE id IN (32415, 32416);\nSELECT id, status, code, sender, recipient, created_at\nFROM text_relays\nWHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'\nORDER BY created_at DESC\nLIMIT 10;\n\nSELECT id, uuid, status, code, sender, recipient, created_at, updated_at\nFROM text_relays\nWHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');\n\n# ***************\nSELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count\nFROM users u\nINNER JOIN activities a ON u.id = .user_id\nWHERE a.type LIKE 'sms%'\nAND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)\nGROUP BY u.id, u.email, u.name, u.softphone_number\nORDER BY sms_count DESC;\n\nselect * from teams where id = 1;\n\nselect * from roles;\n\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1117 and sa.provider = 'hubspot';\nSELECT * FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896050248') = uuid; # 79933459 YES\nSELECT * FROM activities WHERE uuid_to_bin('25529043-8094-4781-927f-4f4da2a8185c') = uuid; # 80186192 NO\nSELECT * FROM crm_configurations WHERE id = 1053;\nSELECT * FROM teams WHERE id = 1117;\nselect * from users where id = 30249;\nselect * from playbooks where id = 5473;\nselect * from playbook_categories where id = 43783;\nselect * from playbook_categories where playbook_id = 5473;\nselect * from crm_fields where id = 659242;\nselect * from crm_field_values where crm_field_id = 659242;\n\nSELECT * FROM crm_field_data fd\n# JOIN crm_fields f ON fd.crm_field_id = f.id\n# JOIN activities a ON fd.activity_id = a.id\nWHERE activity_id = 79933459\n# AND f.crm_provider_id = 'hs_activity_type';\n\n\nSELECT * FROM activity_messages;\nselect * from text_relays where created_at > '2026-05-01';\nselect * from activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;\nselect * from users where team_id = 1 and id IN (18608, 13934, 7160);\nselect * from activities where user_id = 7160 order by id desc limit 10;\n\nselect * from accounts where team_id = 1 and name = 'Column5';\n\nselect * from users where name like '%Subra%'; # 31054, 1117\nselect * from teams where id = 1117;\nselect * from activity_searches where user_id = 31054;\nselect * from activity_search_filters where activity_search_id IN (88882, 88902);","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4911831517688950861
|
-6520523836328617179
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Built-in Preview
Chrome
Firefox
Safari
Sync Changes
Hide This Notification
Code changed:
Hide
3
32
81
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Salesforce;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Facades\Cache;
use Jiminny\Component\Country\CountriesMap;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Enums\CrmObject;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\NoResultsException;
use Jiminny\Integrations\PlaybookResolver;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Playbook;
use Jiminny\Models\Team;
use Jiminny\Models\TeamSettings;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Salesforce\Client;
use Jiminny\Services\Crm\Salesforce\PayloadBuilder;
use Jiminny\Services\Crm\Salesforce\QueryBuilder;
use Jiminny\Services\Crm\Salesforce\QueryHandler;
use Jiminny\Services\Crm\Salesforce\QueryIterator;
use Jiminny\Services\Crm\Salesforce\QueryResults;
use Jiminny\Services\Crm\Salesforce\Service;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
use Tests\Unit\Traits\TestPrivateMethod;
class ServiceTest extends TestCase
{
use TestPrivateMethod;
public function testFetchAndAssociateRelatedActivity(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$payloadBuilder->method('addCustomLogicFieldsPayload')
->willReturnCallback(function ($activity, $payload) {
return $payload;
});
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods(['fetchRelatedActivity', 'getPlaybook', 'getPlaybookCategory', 'updateRecord'])
->getMock();
$serviceMock->expects($this->once())
->method('fetchRelatedActivity')
->willReturn([
'Id' => 'testId',
'Type' => null,
'OwnerId' => 'testerUser',
'Description' => 'Test description',
]);
$user = $this->createMock(User::class);
$team = $this->createMock(Team::class);
$user->method('getAttribute')->with('team')->willReturn($team);
$playbook = $this->createMock(Playbook::class);
$playbook->method('getActivityField')->willReturn(null);
$playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_EVENT);
$serviceMock->expects($this->once())
->method('getPlaybook')
->with($user)
->willReturn($playbook);
$serviceMock->expects($this->never())
->method('getPlaybookCategory');
$serviceMock->expects($this->never())
->method('updateRecord');
$fieldDataRepository = $this->createMock(FieldDataRepository::class);
$fieldDataRepository->method('getActivityFieldData')->willReturn(collect([]));
app()->instance(FieldDataRepository::class, $fieldDataRepository);
$config = $this->createMock(Configuration::class);
$profilesRelation = $this->getMockBuilder(\Illuminate\Database\Eloquent\Relations\HasMany::class)
->disableOriginalConstructor()
->onlyMethods(['get'])
->addMethods(['where'])
->getMock();
$profilesRelation->method('where')->willReturnSelf();
$profilesRelation->method('get')->willReturn(collect([]));
$config->method('profiles')->willReturn($profilesRelation);
$serviceMock->config = $config;
$serviceMock->profile = null;
$actualStartTime = \Carbon\Carbon::now();
$activity = $this->getMockBuilder(Activity::class)
->disableOriginalConstructor()
->onlyMethods(['update', 'hasProspect'])
->getMock();
$activity->method('update')->willReturn(true);
$activity->method('hasProspect')->willReturn(true);
$activity->type = Activity::TYPE_CONFERENCE;
$activity->provider = Activity::PROVIDER_TWILIO;
$activity->lead_id = 1;
$activity->user_id = 0;
$activity->id_string = 'test-activity-id';
$activity->user = $user;
$activity->actual_start_time = $actualStartTime;
$activity->uuid = 'c53d8320-f556-4cee-a2f8-5f232f454ca4';
app()->bind(PlaybookResolver::class, function () use ($user) {
$playbook = $this->createMock(Playbook::class);
$playbookResolver = $this->createMock(PlaybookResolver::class);
$playbookResolver->expects($this->once())
->method('resolvePlaybookByUser')
->with($user)
->willReturn($playbook);
return $playbookResolver;
});
$data = $serviceMock->fetchAndAssociateRelatedActivity($activity);
$this->assertInstanceOf(Activity::class, $data);
$this->assertEquals(Activity::TYPE_CONFERENCE, $data->getType());
$this->assertEquals($actualStartTime->getTimestamp(), $data->getActualStartTime()->getTimestamp());
}
public function testFetchAndAssociateRelatedActivitySkipsForTaskBasedPlaybook(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods(['fetchRelatedActivity', 'getPlaybook'])
->getMock();
$user = $this->createMock(User::class);
$playbook = $this->createMock(Playbook::class);
$playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);
$playbook->method('getId')->willReturn(123);
$serviceMock->expects($this->once())
->method('getPlaybook')
->with($user)
->willReturn($playbook);
$serviceMock->expects($this->never())
->method('fetchRelatedActivity');
$activity = $this->getMockBuilder(Activity::class)
->disableOriginalConstructor()
->onlyMethods(['hasProspect', 'getUuid'])
->getMock();
$activity->method('hasProspect')->willReturn(true);
$activity->method('getUuid')->willReturn('c53d8320-f556-4cee-a2f8-5f232f454ca4');
$activity->type = Activity::TYPE_CONFERENCE;
$activity->actual_start_time = \Carbon\Carbon::now();
$activity->user = $user;
$result = $serviceMock->fetchAndAssociateRelatedActivity($activity);
$this->assertNull($result);
}
public function testFetchAndAssociateRelatedActivityReturnsNullForNonConference(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
$serviceMock = new Service(
client: $client,
payloadBuilder: $payloadBuilder,
eventDispatcher: $eventDispatcher,
countriesMap: $countriesMap,
prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class)
);
$activity = $this->getMockBuilder(Activity::class)
->disableOriginalConstructor()
->getMock();
$activity->type = Activity::TYPE_SOFTPHONE;
$result = $serviceMock->fetchAndAssociateRelatedActivity($activity);
$this->assertNull($result);
}
public function testFetchAndAssociateRelatedActivityReturnsNullWhenNoStartTime(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
$serviceMock = new Service(
client: $client,
payloadBuilder: $payloadBuilder,
eventDispatcher: $eventDispatcher,
countriesMap: $countriesMap,
prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class)
);
$activity = $this->getMockBuilder(Activity::class)
->disableOriginalConstructor()
->getMock();
$activity->type = Activity::TYPE_CONFERENCE;
$activity->actual_start_time = null;
$activity->scheduled_start_time = null;
$result = $serviceMock->fetchAndAssociateRelatedActivity($activity);
$this->assertNull($result);
}
public function testFetchAndAssociateRelatedActivityReturnsNullWhenNoProspect(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods(['getPlaybook'])
->getMock();
$serviceMock->expects($this->never())
->method('getPlaybook');
$activity = $this->getMockBuilder(Activity::class)
->disableOriginalConstructor()
->onlyMethods(['hasProspect', 'getUuid'])
->getMock();
$activity->method('hasProspect')->willReturn(false);
$activity->method('getUuid')->willReturn('c53d8320-f556-4cee-a2f8-5f232f454ca4');
$activity->type = Activity::TYPE_CONFERENCE;
$activity->actual_start_time = \Carbon\Carbon::now();
$result = $serviceMock->fetchAndAssociateRelatedActivity($activity);
$this->assertNull($result);
}
public function testMatchExactlyByEmail(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods([])
->getMock();
$profile = new Profile();
$profile->setAttribute('id', bin2hex(random_bytes(8)));
$serviceMock->profile = $profile;
$team = $this->createMock(Team::class);
$serviceMock->team = $team;
$data = $serviceMock->matchExactlyByEmail(bin2hex(random_bytes(8)) . '[EMAIL]');
$this->assertEquals(null, $data);
}
public function testMatchDomainFromEmail(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$queryIterator = $this->createMock(QueryIterator::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
$config = $this->createMock(Configuration::class);
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->any())
->method('search')
->willReturn($queryIterator);
return $handler;
});
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods(['convertCrmData'])
->getMock();
$profile = new Profile();
$profile->account_fields = 'Field1, Field2, Field3';
$serviceMock->profile = $profile;
$serviceMock->expects($this->once())
->method('convertCrmData')
->willReturn(['test']);
$this->app->bind(QueryBuilder::class, function () {
$queryBuilder = $this->createMock(QueryBuilder::class);
$queryBuilder->expects($this->once())
->method('buildMatchByDomainQuery')
->with('[EMAIL]')
->willReturn('FIND {[EMAIL]} IN ALL FIELDS RETURNING Account(Id)');
return $queryBuilder;
});
$team = $this->createMock(Team::class);
$serviceMock->team = $team;
$serviceMock->config = $config;
$data = $serviceMock->matchByDomain('[EMAIL]');
$this->assertEquals(['test'], $data);
}
public function testBuildTaskSearchFields(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
$service = new Service(
client: $client,
payloadBuilder: $payloadBuilder,
eventDispatcher: $eventDispatcher,
countriesMap: $countriesMap,
prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class)
);
$fields = $service->buildTaskSearchFields();
$expectedFields = ['Id', 'WhoId', 'WhatId', 'AccountId'];
$this->assertEquals($expectedFields, $fields);
}
public function testMapCrmObjects(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
$service = new Service(
client: $client,
payloadBuilder: $payloadBuilder,
eventDispatcher: $eventDispatcher,
countriesMap: $countriesMap,
prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class),
);
$sampleTask = [
'WhoId' => '003sampleWhoId',
'AccountId' => 'sampleAccountId',
'WhatId' => 'sampleWhatId',
];
$activityData = $service->mapCrmObjects($sampleTask);
$expectedActivityData = [
'contact' => '003sampleWhoId',
'account' => 'sampleAccountId',
'opportunity' => 'sampleWhatId',
];
$this->assertEquals($expectedActivityData, $activityData);
}
public function testGetInstalledAppVersion(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
$queryIterator = $this->createMock(QueryIterator::class);
$queryIterator->expects($this->any())
->method('current')->willReturn([
'SubscriberPackageVersion' => [
'MajorVersion' => '1',
'MinorVersion' => '0',
'PatchVersion' => '1',
'BuildNumber' => '0',
],
]);
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->any())
->method('metadata')
->willReturn($queryIterator);
return $handler;
});
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods(array_diff(get_class_methods(Service::class), ['getInstalledAppVersion']))
->getMock();
$version = $serviceMock->getInstalledAppVersion();
$this->assertEquals('1010', $version);
}
public function testSyncProfiles(): void
{
$userToSearch = null;
$team = $this->createMock(Team::class);
$config = $this->createMockedConfiguration();
$config->expects($this->once())
->method('getId')
->willReturn(1);
$salesforceUser = [
'Email' => '[EMAIL]',
'UserPreferencesLightningExperiencePreferred' => true,
'CallCenterId' => '123',
'Id' => '456',
'ProfileId' => '789',
];
app()->bind(QueryBuilder::class, function () use ($userToSearch) {
$queryBuilder = $this->createMock(QueryBuilder::class);
$queryBuilder->expects($this->once())
->method('buildGetUsersQuery')
->with($userToSearch)
->willReturn('SELECT * FROM Users');
return $queryBuilder;
});
$queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults([$salesforceUser], 1, true, null));
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->any())
->method('query')
->with('SELECT * FROM Users')
->willReturn($queryIterator);
return $handler;
});
$user = $this->createMock(User::class);
$user->expects($this->once())
->method('getId')
->willReturn(123);
$this->mockTeamRepository($team, $salesforceUser, $user);
$profileRepository = $this->createMock(ProfileRepository::class);
$profileRepository->expects($this->once())
->method('updateOrCreateProfile')
->with(
$user,
[
'crm_configuration_id' => 1,
'crm_provider_id' => '456',
],
[
'user_id' => 123,
'edition' => Profile::EDITION_LIGHTNING,
'has_external_cti' => true,
'crm_profile_id' => '789',
]
)
->willReturn(new Profile());
$this->app->instance(ProfileRepository::class, $profileRepository);
$serviceMock = $this->getServiceMock();
$serviceMock->team = $team;
$serviceMock->config = $config;
$result = $serviceMock->syncProfiles($userToSearch);
$this->assertNull($result);
}
public function testSyncProfilesEmailIsNull(): void
{
$userToSearch = $this->createMock(User::class);
$salesforceUser = [
'Email' => null,
];
app()->bind(QueryBuilder::class, function () {
$queryBuilder = $this->createMock(QueryBuilder::class);
$queryBuilder->expects($this->once())
->method('buildGetUsersQuery')
->with(null)
->willReturn('SELECT * FROM Users');
return $queryBuilder;
});
$queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults([$salesforceUser], 1, true, null));
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->exactly(2))
->method('query')
->willReturn($queryIterator);
return $handler;
});
$team = $this->createMock(Team::class);
$user = $this->createMock(User::class);
$this->mockTeamRepository($team, $salesforceUser, $user, false);
$config = $this->createMock(Configuration::class);
$serviceMock = $this->getServiceMock();
$serviceMock->team = $team;
$serviceMock->config = $config;
$profile = $serviceMock->syncProfiles(null);
$this->assertNull($profile);
}
public function testSyncProfilesUserToSearchMatchesCurrentUser(): void
{
$userToSearch = $this->createMock(User::class);
$userToSearch->expects($this->once())
->method('getId')
->willReturn(123);
$team = $this->createMock(Team::class);
$config = $this->createMock(Configuration::class);
$config->expects($this->once())
->method('getId')
->willReturn(1);
$salesforceUser = [
'Email' => '[EMAIL]',
'UserPreferencesLightningExperiencePreferred' => true,
'CallCenterId' => '123',
'Id' => '456',
'ProfileId' => '789',
];
app()->bind(QueryBuilder::class, function () use ($userToSearch) {
$queryBuilder = $this->createMock(QueryBuilder::class);
$queryBuilder->expects($this->once())
->method('buildGetUsersQuery')
->with($userToSearch)
->willReturn('SELECT * FROM Users');
return $queryBuilder;
});
$queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults([$salesforceUser], 1, true, null));
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->any())
->method('query')
->with('SELECT * FROM Users')
->willReturn($queryIterator);
return $handler;
});
$user = $this->createMock(User::class);
$user->expects($this->exactly(2))
->method('getId')
->willReturn(123);
$this->mockTeamRepository($team, $salesforceUser, $user);
$profileRepository = $this->createMock(ProfileRepository::class);
$profileRepository->expects($this->once())
->method('updateOrCreateProfile')
->with(
$user,
[
'crm_configuration_id' => 1,
'crm_provider_id' => '456',
],
[
'user_id' => 123,
'edition' => Profile::EDITION_LIGHTNING,
'has_external_cti' => true,
'crm_profile_id' => '789',
]
)
->willReturn(new Profile());
$this->app->instance(ProfileRepository::class, $profileRepository);
$serviceMock = $this->getServiceMock();
$serviceMock->team = $team;
$serviceMock->config = $config;
$profile = $serviceMock->syncProfiles($userToSearch);
$this->assertInstanceOf(Profile::class, $profile);
}
public function testSyncProfilesWithCustomValidation(): void
{
$userToSearch = null;
$team = $this->createMock(Team::class);
$config = $this->createMockedConfiguration();
$config->expects($this->atLeastOnce()) // Changed from once() to atLeastOnce()
->method('getId')
->willReturn(1);
$salesforceUser = [
'Email' => '[EMAIL]',
'UserPreferencesLightningExperiencePreferred' => true,
'CallCenterId' => '123',
'Id' => '456',
'ProfileId' => '789',
'CustomField' => 'CustomValue',
];
$customRules = [
['field' => 'CustomField', 'value' => 'CustomValue'],
];
$this->mockQueryBuilderAndHandler($userToSearch, [$salesforceUser]);
$user = $this->createMock(User::class);
$user->expects($this->once())
->method('getId')
->willReturn(123);
$this->mockTeamRepository($team, $salesforceUser, $user, true, $customRules);
$this->mockProfileRepository($user);
$serviceMock = $this->getServiceMock();
$serviceMock->team = $team;
$serviceMock->config = $config;
$profile = $serviceMock->syncProfiles($userToSearch);
$this->assertNull($profile);
}
public function testSyncProfilesWithCustomValidationFailing(): void
{
$userToSearch = null;
$team = $this->createMock(Team::class);
$config = $this->createMockedConfiguration();
$salesforceUser = [
'Email' => '[EMAIL]',
'UserPreferencesLightningExperiencePreferred' => true,
'CallCenterId' => '123',
'Id' => '456',
'ProfileId' => '789',
'CustomField' => 'WrongValue',
];
$customRules = [
['field' => 'CustomField', 'value' => 'CustomValue'],
];
$this->mockQueryBuilderAndHandler($userToSearch, [$salesforceUser]);
$teamRepository = $this->getMockForAbstractClass(TeamRepository::class, [], '', false, true, true, ['findActiveTeamMemberByEmail', 'getTeamSetting']);
$teamSettings = $this->createMock(TeamSettings::class);
$teamSettings->method('getValueType')
->willReturn('array');
$teamSettings->method('getValue')
->willReturn(json_encode($customRules));
$teamRepository->expects($this->once())
->method('getTeamSetting')
->with($team, 'custom_profile_validation')
->willReturn($teamSettings);
app()->bind(TeamRepository::class, function () use ($teamRepository) {
return $teamRepository;
});
$profileRepository = $this->createMock(ProfileRepository::class);
$profileRepository->expects($this->never())
->method('updateOrCreateProfile');
$this->app->instance(ProfileRepository::class, $profileRepository);
$serviceMock = $this->getServiceMock();
$serviceMock->team = $team;
$serviceMock->config = $config;
$result = $serviceMock->syncProfiles($userToSearch);
$this->assertNull($result);
}
public function testGetContactRolesFromCrm(): void
{
$contactRoles = [
[
'Id' => '1',
'ContactId' => 'Contact1',
'OpportunityId' => 'Opportunity1',
'Opportunity' => ['OwnerId' => 'Owner1'],
'IsPrimary' => true,
'Role' => 'Decision Maker',
],
];
$expectedResponse = [
[
'id' => '1',
'contactId' => 'Contact1',
'opportunityId' => 'Opportunity1',
'ownerId' => 'Owner1',
'isPrimary' => true,
'role' => 'Decision Maker',
],
];
$this->bindQueryIterator($contactRoles);
$serviceMock = $this->getServiceMock();
$data = $serviceMock->getContactRolesFromCrm(now()->subDay());
$this->assertEquals($expectedResponse, $data);
}
public function testGetContactRolesFromCrmNoResult(): void
{
$this->bindQueryIterator([]);
$serviceMock = $this->getServiceMock();
$data = $serviceMock->getContactRolesFromCrm(now()->subDay());
$this->assertEquals([], $data);
}
public function testSyncContactRoles(): void
{
$contactRoles = [
[
'id' => '1',
'contactId' => 'Contact1',
'opportunityId' => 'Opportunity1',
'ownerId' => 'Owner1',
'isPrimary' => true,
'role' => 'Decision Maker',
],
];
app()->bind(ContactRoleRepository::class, function () {
$contactRoleRepository = $this->createMock(ContactRoleRepository::class);
$contactRoleRepository->expects($this->once())
->method('saveContactRoles');
return $contactRoleRepository;
});
$serviceMock = $this->getServiceMock([
'getContactRolesFromCrm',
'syncRemotelyDeletedContactRoles',
'syncContact',
'syncOpportunity',
]);
$config = $this->createMock(Configuration::class);
$hasMany = $this->createMock(HasManyExtended::class);
$hasMany->expects($this->exactly(2))
->method('where')
->willReturn($hasMany);
$hasMany->expects($this->exactly(2))
->method('first')
->willReturn(
$this->createMock(Contact::class),
$this->createMock(Opportunity::class)
);
$config->expects($this->once())
->method('contacts')
->willReturn($hasMany);
$config->expects($this->once())
->method('opportunities')
->willReturn($hasMany);
$serviceMock->config = $config;
$serviceMock->expects($this->once())
->method('getContactRolesFromCrm')
->willReturn($contactRoles);
$serviceMock->expects($this->once())
->method('syncRemotelyDeletedContactRoles');
$data = $serviceMock->syncContactRoles(now()->subDay());
$this->assertEquals(1, $data);
}
public function testSyncRemotelyDeletedContactRoles(): void
{
$contactRoles = [
[
'id' => '1',
'crm_provider_id' => '1',
],
];
app()->bind(QueryHandler::class, function () use ($contactRoles) {
$queryResults = new QueryResults($contactRoles, 1, true, null);
$handler = $this->createMock(QueryHandler::class);
$handler->method('queryDeleted')
->willReturn($queryResults);
return $handler;
});
app()->bind(ContactRoleRepository::class, function () {
$contactRoleRepository = $this->createMock(ContactRoleRepository::class);
$contactRoleRepository->expects($this->once())
->method('deleteContactRoles');
return $contactRoleRepository;
});
$serviceMock = $this->getServiceMock();
$serviceMock->team = $this->createMock(Team::class);
$data = $this->invokePrivateMethod('syncRemotelyDeletedContactRoles', $serviceMock, []);
$this->assertTrue($data);
}
private function bindQueryIterator(array $queryResult): void
{
/** @var Client $client */
$client = $this->createMock(Client::class);
$queryIterator = new QueryIterator(
$client,
new QueryResults($queryResult, 1, true, null)
);
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->any())
->method('query')
->willReturn($queryIterator);
return $handler;
});
}
public static function getOpportunitySortOrderDataProvider(): array
{
return [
'all open recently updated' => [Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED, ['LastModifiedDate DESC', true]],
'all open recently created' => [Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED, ['CreatedDate DESC', true]],
'all open oldest created' => [Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED, ['CreatedDate ASC', true]],
'all recently updated' => [Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED, ['LastModifiedDate DESC', false]],
'default' => ['unknown', ['LastModifiedDate DESC', true]],
];
}
private function createMockedConfiguration(): Configuration
{
$config = $this->createMock(Configuration::class);
$profilesRelation = $this->getMockBuilder(\Illuminate\Database\Eloquent\Relations\HasMany::class)
->disableOriginalConstructor()
->onlyMethods(['get'])
->addMethods(['where', 'first'])
->getMock();
$profilesRelation->method('where')->willReturnSelf();
$profilesRelation->method('get')->willReturn(collect([]));
$profilesRelation->method('first')->willReturn(null);
$config->method('profiles')->willReturn($profilesRelation);
return $config;
}
private function getServiceMock(array $onlyMethods = []): MockObject&Service
{
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$this->createMock(Client::class),
$this->createMock(PayloadBuilder::class),
$this->createMock(Dispatcher::class),
$this->createMock(CountriesMap::class),
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods($onlyMethods)
->getMock();
$serviceMock->profile = $this->createMock(Profile::class);
return $serviceMock;
}
private function mockQueryBuilderAndHandler($userToSearch, $salesforceUsers): void
{
app()->bind(QueryBuilder::class, function () use ($userToSearch) {
$queryBuilder = $this->createMock(QueryBuilder::class);
$queryBuilder->expects($this->once())
->method('buildGetUsersQuery')
->with($userToSearch)
->willReturn('SELECT * FROM Users');
return $queryBuilder;
});
$queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults($salesforceUsers, count($salesforceUsers), true, null));
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->any())
->method('query')
->willReturn($queryIterator);
return $handler;
});
}
private function mockTeamRepository(
Team $team,
array $salesforceUser,
?User $user = null,
bool $userSearch = true,
array $customRules = []
): void {
$teamRepository = $this->getMockForAbstractClass(TeamRepository::class, [], '', false, true, true, ['findActiveTeamMemberByEmail', 'getTeamSetting']);
if ($userSearch) {
$teamRepository->expects($this->once())
->method('findActiveTeamMemberByEmail')
->with($team, $salesforceUser['Email'])
->willReturn($user);
}
$teamSettings = $this->createMock(TeamSettings::class);
$teamSettings->method('getValueType')
->willReturn('array');
$teamSettings->method('getValue')
->willReturn(json_encode($customRules));
$teamRepository->expects($this->once())
->method('getTeamSetting')
->with($team, 'custom_profile_validation')
->willReturn($teamSettings);
app()->bind(TeamRepository::class, function () use ($teamRepository) {
return $teamRepository;
});
}
private function mockProfileRepository(User $user): void
{
$profileRepository = $this->createMock(ProfileRepository::class);
$profileRepository->expects($this->once())
->method('updateOrCreateProfile')
->with(
$user,
[
'crm_configuration_id' => 1,
'crm_provider_id' => '456',
],
[
'user_id' => 123,
'edition' => Profile::EDITION_LIGHTNING,
'has_external_cti' => true,
'crm_profile_id' => '789',
]
)
->willReturn(new Profile());
$this->app->instance(ProfileRepository::class, $profileRepository);
}
public function testBuildEnhancedNoteDecodesHtmlEntities(): void
{
$service = $this->getServiceMock(['createRecord']);
$profile = new Profile();
$profile->setAttribute('crm_provider_id', 'owner-123');
$service->profile = $profile;
$service->expects($this->exactly(2))
->method('createRecord')
->willReturnOnConsecutiveCalls('note-id-123', 'link-id-456');
$bodyWithEntities = 'Welch's current challenges and Facebook's Club';
$result = $this->invokePrivateMethod('buildEnhancedNote', $service, [
'Test Title',
$bodyWithEntities,
'object-id-789',
]);
$this->assertEquals('note-id-123', $result);
}
public function testBuildEnhancedNoteSanitizesWithoutQuotes(): void
{
$service = $this->getServiceMock(['createRecord']);
$profile = new Profile();
$profile->setAttribute('crm_provider_id', 'owner-456');
$service->profile = $profile;
$service->expects($this->exactly(2))
->method('createRecord')
->willReturnCallback(function ($type, $data) {
if ($type === 'ContentNote') {
$decoded = base64_decode($data['Content']);
$this->assertStringContainsString("Welch's", $decoded);
$this->assertStringNotContainsString(''', $decoded);
$this->assertStringNotContainsString('&#039;', $decoded);
$this->assertStringContainsString('<script>', $decoded);
return 'note-id-456';
}
return 'link-id-789';
});
$bodyWithMixedContent = "Welch's and <script>alert('xss')</script>";
$result = $this->invokePrivateMethod('buildEnhancedNote', $service, [
'Test Title',
$bodyWithMixedContent,
'object-id-123',
]);
$this->assertEquals('note-id-456', $result);
}
public function testBuildEnhancedNoteConvertsLineBreaks(): void
{
$service = $this->getServiceMock(['createRecord']);
$profile = new Profile();
$profile->setAttribute('crm_provider_id', 'owner-789');
$service->profile = $profile;
$service->expects($this->exactly(2))
->method('createRecord')
->willReturnCallback(function ($type, $data) {
if ($type === 'ContentNote') {
$decoded = base64_decode($data['Content']);
$this->assertStringContainsString('<br>', $decoded);
$this->assertStringNotContainsString('<br />', $decoded);
return 'note-id-789';
}
return 'link-id-012';
});
$bodyWithLineBreaks = "Line 1\nLine 2\nLine 3";
$result = $this->invokePrivateMethod('buildEnhancedNote', $service, [
'Test Title',
$bodyWithLineBreaks,
'object-id-456',
]);
$this->assertEquals('note-id-789', $result);
}
public function testBuildEnhancedNoteHandlesComplexScenario(): void
{
$service = $this->getServiceMock(['createRecord']);
$profile = new Profile();
$profile->setAttribute('crm_provider_id', 'owner-complex');
$service->profile = $profile;
$service->expects($this->exactly(2))
->method('createRecord')
->willReturnCallback(function ($type, $data) {
if ($type === 'ContentNote') {
$decoded = base64_decode($data['Content']);
$this->assertStringContainsString("Welch's", $decoded);
$this->assertStringContainsString("Facebook's Club", $decoded);
$this->assertStringContainsString("Arctics'", $decoded);
$this->assertStringNotContainsString(''', $decoded);
$this->assertStringNotContainsString('&#039;', $decoded);
$this->assertStringContainsString('<br>', $decoded);
$this->assertStringContainsString('<', $decoded);
$this->assertStringContainsString('>', $decoded);
return 'note-complex';
}
return 'link-complex';
});
$complexBody = "Summary:\n---------\nThe call focused on understanding Welch's current challenges and exploring how Arctics' Revenue Growth Management solutions could support their strategic goals.\n\n• John SMith discussed his role as a category advisor for Google and Facebook's Club, emphasizing the importance of market research and advising on product assortment.\n• Madona introduced Arctics' Virtual Shoppers AI, which simulates consumer <behavior> to optimize pricing and promotional strategies.";
$result = $this->invokePrivateMethod('buildEnhancedNote', $service, [
'Jiminny Transcription Summary',
$complexBody,
'task-id-001',
]);
$this->assertEquals('note-complex', $result);
}
public function testSyncRemotelyDeletedObjectsWithErrorHandlingSuccess(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$prospectPhotoPathService,
])
->onlyMethods(['syncRemotelyDeletedObjects'])
->getMock();
// Mock team
$team = $this->createMock(Team::class);
$team->method('getUuid')->willReturn('team-uuid-123');
$serviceMock->team = $team;
// Expect syncRemotelyDeletedObjects to be called once and succeed
$serviceMock->expects($this->once())
->method('syncRemotelyDeletedObjects')
->with(\Jiminny\Enums\CrmObject::ACCOUNT);
// Call the protected method using reflection
$reflection = new \ReflectionClass($serviceMock);
$method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');
$method->setAccessible(true);
// Should not throw any exceptions
$method->invoke($serviceMock, \Jiminny\Enums\CrmObject::ACCOUNT);
$this->assertTrue(true); // Test completed successfully
}
public function testSyncRemotelyDeletedObjectsWithErrorHandlingFailure(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$prospectPhotoPathService,
])
->onlyMethods(['syncRemotelyDeletedObjects'])
->getMock();
// Mock team
$team = $this->createMock(Team::class);
$team->method('getUuid')->willReturn('team-uuid-456');
$serviceMock->team = $team;
// Mock logger to verify warning is logged
$logger = $this->createMock(\Psr\Log\LoggerInterface::class);
// Use reflection to set the protected logger property
$reflection = new \ReflectionClass($serviceMock);
$loggerProperty = $reflection->getProperty('logger');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($serviceMock, $logger);
$exception = new \Exception('Sync failed due to API error');
// Expect syncRemotelyDeletedObjects to throw an exception
$serviceMock->expects($this->once())
->method('syncRemotelyDeletedObjects')
->with(\Jiminny\Enums\CrmObject::CONTACT)
->willThrowException($exception);
// Expect warning to be logged with correct message and parameters
$logger->expects($this->once())
->method('warning')
->with(
'[Salesforce] Remotely deleted objects sync failed',
[
'objectType' => 'contact',
'teamId' => 'team-uuid-456',
'reason' => 'Sync failed due to API error',
]
);
// Call the protected method using reflection
$reflection = new \ReflectionClass($serviceMock);
$method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');
$method->setAccessible(true);
// Should not re-throw the exception, just log it
$method->invoke($serviceMock, \Jiminny\Enums\CrmObject::CONTACT);
$this->assertTrue(true); // Test completed successfully
}
public function testSyncRemotelyDeletedObjectsWithErrorHandlingWithLogParams(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$prospectPhotoPathService,
])
->onlyMethods(['syncRemotelyDeletedObjects'])
->getMock();
// Mock team
$team = $this->createMock(Team::class);
$team->method('getUuid')->willReturn('team-uuid-789');
$serviceMock->team = $team;
// Mock logger to verify warning is logged
$logger = $this->createMock(\Psr\Log\LoggerInterface::class);
// Use reflection to set the protected logger property
$loggerReflection = new \ReflectionClass($serviceMock);
$loggerProperty = $loggerReflection->getProperty('logger');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($serviceMock, $logger);
$exception = new \Exception('Network timeout');
// Expect syncRemotelyDeletedObjects to throw an exception
$serviceMock->expects($this->once())
->method('syncRemotelyDeletedObjects')
->with(\Jiminny\Enums\CrmObject::OPPORTUNITY)
->willThrowException($exception);
// Additional log parameters
$logParams = [
'syncType' => 'full',
'batchSize' => 100,
];
// Expect warning to be logged with merged parameters
$logger->expects($this->once())
->method('warning')
->with(
'[Salesforce] Remotely deleted objects sync failed',
[
'objectType' => 'opportunity',
'teamId' => 'team-uuid-789',
'reason' => 'Network timeout',
'syncType' => 'full',
'batchSize' => 100,
]
);
// Call the protected method using reflection
$reflection = new \ReflectionClass($serviceMock);
$method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');
$method->setAccessible(...
|
85744
|
NULL
|
NULL
|
NULL
|
|
85744
|
2939
|
12
|
2026-05-28T13:04:39.615208+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973479615_m2.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rapstomViewNeweNNCCoocKelucioWindowFV faVsco.|s ~# rapstomViewNeweNNCCoocKelucioWindowFV faVsco.|s ~#12121 on JY-20963-fx-eSarvicc.ohwnapers.oh©ActivityControlier.php• HttpIntegrationslinteractionsJobshwioDonciwcr.onyTeom.onec) e enupn;E custom.logA console (STAGING)A SF fiminny@localhost)HSJocal (jiminny@blocalhost)console (PROD) X2019class ServiceTest extends TestCasepublic function testParseContactMobileWithValidNumberO: voida Maili ModeleNotificationsvoseiversSocialAccountOoserverlest2674Sresult = self::invokePrivateMethod( methodName: 'parseContactMobile', Sservice. [null. Screbatal)e78)thts-sassertNorultSresulthiso assent sanet exocctesUserRoleObserverTest.pho7es=78%-78lPolcies2699public function testParsecontactHob1lelstruncatedtoz5chars: vo1c26322633SseruiceThisosoerSenys celockioMlonaluchen 42:05558s>EACtVIYla ActivityProvider:Sermlata = T"MobslePhone" = Sonolluchen718711=712713=724i CalendarTeonterencoa erm→Rulihorclose Sresult = self::invokePrivateMethod( methodName: "parseContactMobile', Sservice, [nutz, ScreDatal): 715Wthss-SaccentMotMullScesultpublic function testParseContactMobfleWithCountryCode@: voidTin Helners2644eveesuoocservcnockoScrmbata = 'MobilePhone' => *07911123456'1#Ti HibsootD IntegrationApplllistenars264626472649PipedriveImSolestoroe› D Fields2650D OpportunityMatcherOpportunitySyncStratel ProsoectSearchStratecim sancettaisClientTest.phoC DecorateActivityTest.o( DeleteObiocts TraitTest 2724anhnafniioastestoh© GetActivitvFieldNameT 2726 0>[EMAIL]© QuervHandlerTest.oha©QuerviteratorTest.ohr©QuervResuitsTests.ohSresult = selt::nvokePrzverehez hodl methoowamethis-sassertNotNulSresulosthis-yassertStringStartsiottheorehc"44 Sresultpublic functsion testinoort0pportunitvSkiosllhenProfileNotFoundO= voidi....Cass Has"envsytanded aytends Hastanybubtic function whereot...public function firstot...HHHI IBETIIIMEPRRBERRRC Semceltestoo@SwncRatchRedicSenics@ RaseSeniiceTest oho(CacharCrmSanicehom+ 1of2editsAccept Fle x= X. Reject File oxQTy: AutowINTERVAL 38 DAY.by 0.z0, u.caszlu.name u.sotcphone nunbenBY sns_count DESC;* fron teans where id = 1;NCAT(u.id, CASE WHEN U.id = t.ouner_id THEN ' (oaner)' ELSE ** END) AS user_id,ouner_1d FROh soclal,accounts saSeNG MOeans t.nc->1Sontd M.teandU.tean_id = 1117 and sa.provider = "hubspot":*EROM actMtes WHEOF Wund to binl18a2tetth-2147-1817.0144-40480695926801= Musd: " 70033459 VES* FROM activities WHERE uuid_to_bin('[CREDIT_CARD]-927f-4f4da2a8185c') = uuid: # 80186192 NO*EROM eon confaurazons MHEPF 3d = 1953%* FROM teans WHERE 1d = 1117:* fron users where id = 30249:* fron playbooks where id = 5473;* fron playbook categonies where id = 43783:* fron playbook categories where playbook_id = 5473:* fron crn_fields where id = 659242:* fron cra_field values where cra_field_id = 659242:* FROM Crn_field_dato fdcrnfields f ON fd.crm field id = f.ioacrviies a UN to.acovity 1d = a.10activity_id = 79933459f.crn_provider_id = 'hs activity_type'* FROM activity_messages;where created at > *2826-05-91*•* fron activities where user_id IN (7160, 18608) and created_at > ^2026-05-22' order by id desc;* fron users where team id = 1 and id TN (18688, 13934. 7169)=* fron activities where user_id = 7160 order by id desc linit 10;Toon tasne whanda4* fron activity_searches where user_id = 31854*Eoon sctkusty coanch filitane nhans sctfusty conoch SA TM CARRR2 RRO02# console fiaum045 A1 A41 У 66 4TO0У L7Thu 28 May 16:04:39ServiceTesteeendhles orchwerwicee+0.testsruexec dovkers/crm/3 bestore 5e/vicer/stmphpy - iterwenest/brseContact' 2561PHPUnit 11.5.55 by Sebastian Bergmann and contributors.contelourarcion.Tno.e/uminnyl chounx=11/ 11 (100%Tine: 00:10.908, Memory: 66.00 MEAl 11 new and existing testParseContact* tests pass. The deprecations are pre-existing PHPUnit warningsLet ma aiso ar'cklv nin tha ull SarufcaTast to contimm nothioa ie brokaa.• decter exe sdrcker /era/3 bestore Servioeeeitetnay 55 php a5vendor/bin/phpun:PHPUnzt 11.5.55 by Sebastzan Beramann and contributors.Confiquration:/home/siminny/phpunit.xmD.•DODOD..•••D000.DD.......••• 65 / 73 ( 894TIeA: 60:21 704 MamArus oA ao MoA.7s tests pass. Herc's a summary of what was added:oo eoesThe PR already covered empty/null early-return paths. The missing coverage was for the happy paths — added 7newssanrsctontact phos•testParseContactPhoneWithValidNumber - E.164 number parses correctly, ext is nul?"esParsccontad Phonewnextens ton -moerwihler ucotttoyerracthe orensottastpareacontsctPhanafytictruncatetto10charc=sytwttlanowthom.th/chhre.ootethindstodltGtruehtaetbareacontnetPhondthrountrtodestocallkoimottheo.watowmeine.cotntmcooparseContactMobiletestparsctonthe wontethaldwuher- Eo numoeteturnd noetActPAreAGontcMahsnTetrunestMhT/20Ghhre-Mimawtlwaw than.2fchare note thinastorlGtrehedtACtOsrcAGAnt.CMAKSlWWAthGAuAtCUCAA.FTAAITIKmANATMeALItAr/mcima.AMintrv.chGunl-1tile +80 2Ask anything (XOL)AdhotvAccept allKwodeurlhimewwkres*4 space...
|
NULL
|
3110981459662262920
|
NULL
|
visual_change
|
ocr
|
NULL
|
rapstomViewNeweNNCCoocKelucioWindowFV faVsco.|s ~# rapstomViewNeweNNCCoocKelucioWindowFV faVsco.|s ~#12121 on JY-20963-fx-eSarvicc.ohwnapers.oh©ActivityControlier.php• HttpIntegrationslinteractionsJobshwioDonciwcr.onyTeom.onec) e enupn;E custom.logA console (STAGING)A SF fiminny@localhost)HSJocal (jiminny@blocalhost)console (PROD) X2019class ServiceTest extends TestCasepublic function testParseContactMobileWithValidNumberO: voida Maili ModeleNotificationsvoseiversSocialAccountOoserverlest2674Sresult = self::invokePrivateMethod( methodName: 'parseContactMobile', Sservice. [null. Screbatal)e78)thts-sassertNorultSresulthiso assent sanet exocctesUserRoleObserverTest.pho7es=78%-78lPolcies2699public function testParsecontactHob1lelstruncatedtoz5chars: vo1c26322633SseruiceThisosoerSenys celockioMlonaluchen 42:05558s>EACtVIYla ActivityProvider:Sermlata = T"MobslePhone" = Sonolluchen718711=712713=724i CalendarTeonterencoa erm→Rulihorclose Sresult = self::invokePrivateMethod( methodName: "parseContactMobile', Sservice, [nutz, ScreDatal): 715Wthss-SaccentMotMullScesultpublic function testParseContactMobfleWithCountryCode@: voidTin Helners2644eveesuoocservcnockoScrmbata = 'MobilePhone' => *07911123456'1#Ti HibsootD IntegrationApplllistenars264626472649PipedriveImSolestoroe› D Fields2650D OpportunityMatcherOpportunitySyncStratel ProsoectSearchStratecim sancettaisClientTest.phoC DecorateActivityTest.o( DeleteObiocts TraitTest 2724anhnafniioastestoh© GetActivitvFieldNameT 2726 0>[EMAIL]© QuervHandlerTest.oha©QuerviteratorTest.ohr©QuervResuitsTests.ohSresult = selt::nvokePrzverehez hodl methoowamethis-sassertNotNulSresulosthis-yassertStringStartsiottheorehc"44 Sresultpublic functsion testinoort0pportunitvSkiosllhenProfileNotFoundO= voidi....Cass Has"envsytanded aytends Hastanybubtic function whereot...public function firstot...HHHI IBETIIIMEPRRBERRRC Semceltestoo@SwncRatchRedicSenics@ RaseSeniiceTest oho(CacharCrmSanicehom+ 1of2editsAccept Fle x= X. Reject File oxQTy: AutowINTERVAL 38 DAY.by 0.z0, u.caszlu.name u.sotcphone nunbenBY sns_count DESC;* fron teans where id = 1;NCAT(u.id, CASE WHEN U.id = t.ouner_id THEN ' (oaner)' ELSE ** END) AS user_id,ouner_1d FROh soclal,accounts saSeNG MOeans t.nc->1Sontd M.teandU.tean_id = 1117 and sa.provider = "hubspot":*EROM actMtes WHEOF Wund to binl18a2tetth-2147-1817.0144-40480695926801= Musd: " 70033459 VES* FROM activities WHERE uuid_to_bin('[CREDIT_CARD]-927f-4f4da2a8185c') = uuid: # 80186192 NO*EROM eon confaurazons MHEPF 3d = 1953%* FROM teans WHERE 1d = 1117:* fron users where id = 30249:* fron playbooks where id = 5473;* fron playbook categonies where id = 43783:* fron playbook categories where playbook_id = 5473:* fron crn_fields where id = 659242:* fron cra_field values where cra_field_id = 659242:* FROM Crn_field_dato fdcrnfields f ON fd.crm field id = f.ioacrviies a UN to.acovity 1d = a.10activity_id = 79933459f.crn_provider_id = 'hs activity_type'* FROM activity_messages;where created at > *2826-05-91*•* fron activities where user_id IN (7160, 18608) and created_at > ^2026-05-22' order by id desc;* fron users where team id = 1 and id TN (18688, 13934. 7169)=* fron activities where user_id = 7160 order by id desc linit 10;Toon tasne whanda4* fron activity_searches where user_id = 31854*Eoon sctkusty coanch filitane nhans sctfusty conoch SA TM CARRR2 RRO02# console fiaum045 A1 A41 У 66 4TO0У L7Thu 28 May 16:04:39ServiceTesteeendhles orchwerwicee+0.testsruexec dovkers/crm/3 bestore 5e/vicer/stmphpy - iterwenest/brseContact' 2561PHPUnit 11.5.55 by Sebastian Bergmann and contributors.contelourarcion.Tno.e/uminnyl chounx=11/ 11 (100%Tine: 00:10.908, Memory: 66.00 MEAl 11 new and existing testParseContact* tests pass. The deprecations are pre-existing PHPUnit warningsLet ma aiso ar'cklv nin tha ull SarufcaTast to contimm nothioa ie brokaa.• decter exe sdrcker /era/3 bestore Servioeeeitetnay 55 php a5vendor/bin/phpun:PHPUnzt 11.5.55 by Sebastzan Beramann and contributors.Confiquration:/home/siminny/phpunit.xmD.•DODOD..•••D000.DD.......••• 65 / 73 ( 894TIeA: 60:21 704 MamArus oA ao MoA.7s tests pass. Herc's a summary of what was added:oo eoesThe PR already covered empty/null early-return paths. The missing coverage was for the happy paths — added 7newssanrsctontact phos•testParseContactPhoneWithValidNumber - E.164 number parses correctly, ext is nul?"esParsccontad Phonewnextens ton -moerwihler ucotttoyerracthe orensottastpareacontsctPhanafytictruncatetto10charc=sytwttlanowthom.th/chhre.ootethindstodltGtruehtaetbareacontnetPhondthrountrtodestocallkoimottheo.watowmeine.cotntmcooparseContactMobiletestparsctonthe wontethaldwuher- Eo numoeteturnd noetActPAreAGontcMahsnTetrunestMhT/20Ghhre-Mimawtlwaw than.2fchare note thinastorlGtrehedtACtOsrcAGAnt.CMAKSlWWAthGAuAtCUCAA.FTAAITIKmANATMeALItAr/mcima.AMintrv.chGunl-1tile +80 2Ask anything (XOL)AdhotvAccept allKwodeurlhimewwkres*4 space...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85743
|
2938
|
10
|
2026-05-28T13:04:38.707379+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973478707_m1.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2• • 0ShellEditViewSessionScripts|ProfilesW iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKER881DEV (docker)₴82-zshN3screenpipe"O ₴4-zshapp/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php348+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec-it docker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|85ec2-user@ip-10-30-129-...100% <78• Thu 28 May 16:04:381₴1ec2-user@ip-10-30-140-...₴7...
|
NULL
|
1264619683831542745
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2• • 0ShellEditViewSessionScripts|ProfilesW iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKER881DEV (docker)₴82-zshN3screenpipe"O ₴4-zshapp/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php348+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec-it docker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|85ec2-user@ip-10-30-129-...100% <78• Thu 28 May 16:04:381₴1ec2-user@ip-10-30-140-...₴7...
|
85741
|
NULL
|
NULL
|
NULL
|
|
85742
|
2939
|
11
|
2026-05-28T13:04:34.664998+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973474664_m2.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rapstomEV faVsco,ls ~ViewCooc#12121 on JY-20963-fo rapstomEV faVsco,ls ~ViewCooc#12121 on JY-20963-fox-hС НttpIntegrations@ interactionsJobsO MailC Modelsanoureatonsvoseivers© SocialAccountObserverTest,;C UserObserverTest.phpE UserRoleObserverTest.phpPolcies>EACtVIYActivityProviders@ CalendarTeonterenco•RullmeenClose& Copper@ HubspotListenersPipedriveSalesforce• FieidsOpportunityMatcherOpportunitySyncStrateProspectSearchStrateg& Service Traits© ClientTest.php© DecorateActivity Test.p© DeleteObjects TraitTest© FieidDefinitions Test.ph© GetActivityFieidNameT© PayloadBullder Test.phyE QueryBullder Test.php© QueryHandler Test.php©QuerviteratorTest.ohr© QueryResultsTests.phpC Semceltestoo© SyncBatchRedisService@ RaseSeniiceTest oho(el CacherCrm Senvicaßeco?Windowwnapers.ohyTeom.one©ActivityController.phpA SF (iminny@localhost)ConsoNe (STAGING)DP000• TXAUtOvBY u.id, u.email, u.name, u.softphone_number* fron teans where id = 1;A console (EU?045 A1 441 X66 ^manesoaceeses ui NerwcestearesoneCurssirvcaloslexcondsesstpublic function testFetchAndAssociateRelatedActivity(): voidSclient = Sthis->createMock( originsSpayLoadBuilder = Sthis->createMock( originalClassNgSpayLoadBuilder->nethod( constralClient::class):= PayloadBuilden::class);ScountriesMap = Sthis-›createMock( criginalClassNe/** Bvar ServicesMockObject SserviceMock */SserviceMock = Sthis->getMockßuilder( className: Service::class)->setConstructorArgs([Sclient.SoayLoadbuzloersevencurspaccherScountriesMap,Sthis->createMock( origina/ClassN>oncynechodstecchketacecacc.v.cy→>аскносkонwcthoekoseyocerReiaho>oneo->mnadl ciTerchie fred.cewiy'-sweeerumnia'Description' => 'Test description'.Steam = Sthis-›createMock( originTean: :class);"aetattobute"oythltnroumdt"tean"oyil Return SteamPlaybook::class);Splaybook->method (totaf2ndigetAd V Accept Fle X--X PeRet6Mn C-%800nulU);= 709710711=722733fron text_relays where created_at > '2026-85-01*:fron activities where user id IN (7160, 18608) and created_at > ^2026-05-22' order by id desc:* fron users where team_id = 1 and id IN (18608, 13934, 7160);* fron activities where user_id = 7160 order by id desc linit 10;* fron teans where id = 1117:fron activity_search_filters nhere activity_search_id IN (88882, 88902)TO0У L7Inu cowoy 1o:04.54U ServiceTest ~Cascadeles orchwerwicee+0.decker nxe dovkces/cr/5 best ree/Servicee/31.php hervendsr/orsepontact: 2261PHPUnit 11.5.55 by Sebastian Bergnann and contributors.PHP 8.S.Sconteouratciont no.e/tminayohounsx"11/ 11 (100%Tine: 00:10.908, Memory: 66.00 MBAl 11 new and existing testParseContact* tests pass. The deprecations are pre-existing PHPUnit warningsLet me also quickly run the full ServiceTest to confirm nothing is broken:o desterext deckee/er/ lestore Serviceestphg s h ventaen -2/b3n/phpunitPHPUnzt 11.5.55 by Sebastzan Beramann and contributors.Configuration: /hose/jiminny/phpunit.xmlD.•DODOD..•••D000.DD.......••• 65 / 73 ( 894Time: 00:21.744, Memory: 94.00 MBA.7s tests pass. Herc's a summary of what was added:oo eoesnewssThe PR already covered empty/null early-return paths. The missing coverage was for the happy paths - added 7parseContactPhone• testParseContactPhonewithValidVumber - E.164 number parses correctly, ext is nutl"esParsccontad Phontwnexens ton -Tmoer winleroconttoty erracthe exensotactpareacontsctPhonaFyttetruncatettotochare=sytwttolonosthm.th/chhre.abtetnindstodllGtru1taetbareacontnetPhondthrount.rtodesloclkoim.tthto.totowmeine.cotntrv.conparseContactMobitetestParsctontae Nonathaiddwuher - Eo numoee teturnd noek•testParseContactMobileIsTruncatedTo25Chars - Number longer than 25 chars gets truncated (Str:: 1iml•testParseContactMob{leW{thCountryCode - Local UK mobile resolved to +44. using country codeB ul -1tile +80 2Accept allAsk anything (XOL)" PodnAdhotvutwoderesmes2 4 space:...
|
NULL
|
4973697426431662652
|
NULL
|
click
|
ocr
|
NULL
|
rapstomEV faVsco,ls ~ViewCooc#12121 on JY-20963-fo rapstomEV faVsco,ls ~ViewCooc#12121 on JY-20963-fox-hС НttpIntegrations@ interactionsJobsO MailC Modelsanoureatonsvoseivers© SocialAccountObserverTest,;C UserObserverTest.phpE UserRoleObserverTest.phpPolcies>EACtVIYActivityProviders@ CalendarTeonterenco•RullmeenClose& Copper@ HubspotListenersPipedriveSalesforce• FieidsOpportunityMatcherOpportunitySyncStrateProspectSearchStrateg& Service Traits© ClientTest.php© DecorateActivity Test.p© DeleteObjects TraitTest© FieidDefinitions Test.ph© GetActivityFieidNameT© PayloadBullder Test.phyE QueryBullder Test.php© QueryHandler Test.php©QuerviteratorTest.ohr© QueryResultsTests.phpC Semceltestoo© SyncBatchRedisService@ RaseSeniiceTest oho(el CacherCrm Senvicaßeco?Windowwnapers.ohyTeom.one©ActivityController.phpA SF (iminny@localhost)ConsoNe (STAGING)DP000• TXAUtOvBY u.id, u.email, u.name, u.softphone_number* fron teans where id = 1;A console (EU?045 A1 441 X66 ^manesoaceeses ui NerwcestearesoneCurssirvcaloslexcondsesstpublic function testFetchAndAssociateRelatedActivity(): voidSclient = Sthis->createMock( originsSpayLoadBuilder = Sthis->createMock( originalClassNgSpayLoadBuilder->nethod( constralClient::class):= PayloadBuilden::class);ScountriesMap = Sthis-›createMock( criginalClassNe/** Bvar ServicesMockObject SserviceMock */SserviceMock = Sthis->getMockßuilder( className: Service::class)->setConstructorArgs([Sclient.SoayLoadbuzloersevencurspaccherScountriesMap,Sthis->createMock( origina/ClassN>oncynechodstecchketacecacc.v.cy→>аскносkонwcthoekoseyocerReiaho>oneo->mnadl ciTerchie fred.cewiy'-sweeerumnia'Description' => 'Test description'.Steam = Sthis-›createMock( originTean: :class);"aetattobute"oythltnroumdt"tean"oyil Return SteamPlaybook::class);Splaybook->method (totaf2ndigetAd V Accept Fle X--X PeRet6Mn C-%800nulU);= 709710711=722733fron text_relays where created_at > '2026-85-01*:fron activities where user id IN (7160, 18608) and created_at > ^2026-05-22' order by id desc:* fron users where team_id = 1 and id IN (18608, 13934, 7160);* fron activities where user_id = 7160 order by id desc linit 10;* fron teans where id = 1117:fron activity_search_filters nhere activity_search_id IN (88882, 88902)TO0У L7Inu cowoy 1o:04.54U ServiceTest ~Cascadeles orchwerwicee+0.decker nxe dovkces/cr/5 best ree/Servicee/31.php hervendsr/orsepontact: 2261PHPUnit 11.5.55 by Sebastian Bergnann and contributors.PHP 8.S.Sconteouratciont no.e/tminayohounsx"11/ 11 (100%Tine: 00:10.908, Memory: 66.00 MBAl 11 new and existing testParseContact* tests pass. The deprecations are pre-existing PHPUnit warningsLet me also quickly run the full ServiceTest to confirm nothing is broken:o desterext deckee/er/ lestore Serviceestphg s h ventaen -2/b3n/phpunitPHPUnzt 11.5.55 by Sebastzan Beramann and contributors.Configuration: /hose/jiminny/phpunit.xmlD.•DODOD..•••D000.DD.......••• 65 / 73 ( 894Time: 00:21.744, Memory: 94.00 MBA.7s tests pass. Herc's a summary of what was added:oo eoesnewssThe PR already covered empty/null early-return paths. The missing coverage was for the happy paths - added 7parseContactPhone• testParseContactPhonewithValidVumber - E.164 number parses correctly, ext is nutl"esParsccontad Phontwnexens ton -Tmoer winleroconttoty erracthe exensotactpareacontsctPhonaFyttetruncatettotochare=sytwttolonosthm.th/chhre.abtetnindstodllGtru1taetbareacontnetPhondthrount.rtodesloclkoim.tthto.totowmeine.cotntrv.conparseContactMobitetestParsctontae Nonathaiddwuher - Eo numoee teturnd noek•testParseContactMobileIsTruncatedTo25Chars - Number longer than 25 chars gets truncated (Str:: 1iml•testParseContactMob{leW{thCountryCode - Local UK mobile resolved to +44. using country codeB ul -1tile +80 2Accept allAsk anything (XOL)" PodnAdhotvutwoderesmes2 4 space:...
|
85740
|
NULL
|
NULL
|
NULL
|
|
85741
|
2938
|
9
|
2026-05-28T13:04:34.562166+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973474562_m1.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2• • 0ShellEditViewSessionScripts|ProfilesW iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKERO ₴1DEV (docker)₴82-zshN3screenpipe"O ₴4-zshapp/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php348+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec-it docker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|85ec2-user@ip-10-30-129-...100% <78• Thu 28 May 16:04:341₴1ec2-user@ip-10-30-140-...₴7...
|
NULL
|
-7589044116343815266
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2• • 0ShellEditViewSessionScripts|ProfilesW iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKERO ₴1DEV (docker)₴82-zshN3screenpipe"O ₴4-zshapp/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php348+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec-it docker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|85ec2-user@ip-10-30-129-...100% <78• Thu 28 May 16:04:341₴1ec2-user@ip-10-30-140-...₴7...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85740
|
2939
|
10
|
2026-05-28T13:04:25.522503+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973465522_m2.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rapstomEV favscojs ~ViewCooc#12121 on JY-20963-fox rapstomEV favscojs ~ViewCooc#12121 on JY-20963-fox-hС НttpIntegrations@ interactionsJobsO MailC Modelsanoureatonsvoseivers© SocialAccountObserverTest,;C UserObserverTest.phpE UserRoleObserverTest.phpPolcies>EACtVIYActivityProviders@ CalendarTeonterenco•RullmeenClose& Copper@ HubspotListenersPipedriveSalesforce• FieidsOpportunityMatcherOpportunitySyncStrateProspectSearchStrateg& Service Traits© ClientTest.php© DecorateActivity Test.p© DeleteObjects TraitTest© FieidDefinitions Test.ph© GetActivityFieidNameT© PayloadBullder Test.phyE QueryBullder Test.php© QueryHandler Test.php©QuerviteratorTest.ohr© QueryResultsTests.phpC Semceltestoo© SyncBatchRedisService@ RaseSeniiceTest oho(el CacherCrm Senvicaßeco?3g% 9Windowwnapers.ohyTeom.one©ActivityController.phpA SF (iminny@localhost)ConsoNe (STAGING)DP000• TXAUtOvBY u.id, u.enail, u.name, u.softphone_number* fron teans where id = 1;A console (EU?045 A1 441 X66 ^Curssirvcaloslexcondsesstpublic function testFetchAndAssociateRelatedActivity(): voidSclient = Sthis->createMock( originsSpayLoadBuilder = Sthis->createMock( originalClassNgSpayLoadBuilder->nethod( constraClient::class):= PayloadBuilder::class):ScountriesMap = Sthis->createMock( origh/** Bvar ServicesMockObject SserviceMock */SserviceMock = Sthis->getMockßuilder( className: Service::class)->setConstructorArgs([Sclient.SoayLoadbuzloersevencurspaccherScountriesMap,Sthis->createMock( origina/ClassN>oncynechodstecchketacecacc.v.cy→>аскносkонwcthoekoseyocerReiaho>oneo->mnadl ci"fetchRelatedActävity")-sweeerumnia'Description' => 'Test description'.Steam = Sthis->createMock( orig)Tean: :class);"aetattobute" oythitnroums"tean"oyil Return SteamPlaybook::class);Splaybook->method(toi.af 2mdMOcLAd ~ ACCept Fle X- bX PeRet 5Mn ( 359u0DE nULU:SpLaybook-›method( constraint: 'getActivityType') -›willReturn( value: PLaybook::ACTIVITY_TYPE_EVENT):= 709710711=722733fron text_relays where created_at > *2026-85-01*)fron activities where user_id IN (7160, 18608) and created_at › '2026-05-22' order by id desc;* fron users where team_id = 1 and id IN (18608, 13934, 7160);* fron activities where user_id = 7160 order by id desc Linit 10;fron teans where id = 1117;fron activity_search_filters nhere activity_search_id IN (88882, 88902)TO0У L7Thu 28 May 16:04:25U ServiceTest ~Cascadelesorceserwiceedecker nxe dovkces/cr/5 best ree/Servicee/31.php hervendsr/orsepontact: 2261PHPUnit 11.5.55 by Sebastian Bergnann and contributors.PHP 8.S.Scontelouratciontnore/tinnyl ohounx"+0.11/ 11 (100%Tine: 00:10.908, Memory: 66.00 MBAl 11 new and existing testParseContact* tests pass. The deprecations are pre-existing PHPUnit warningsLet me also quickly run the full ServiceTest to confirm nothing is broken:o desterext deckee/er/ lestore Serviceestphg s h ventaen -2/b3n/phpunitPHPUnzt 11.5.55 by Sebastzan Beramann and contributors.Configuration: /hose/jiminny/phpunit.xmlD.•DODOD..•••D000.DD.......••• 65 / 73 ( 894Time: 00:21.744, Memory: 94.00 MBA.7s tests pass. Herc's a summary of what was added.oo eoesThe PR already covered empty/null early-return paths. The missing coverage was for the happy paths - added 7newssparseContactPhone• testParseContactPhonewithValidNVumber - E.164 number parses correctly, ext is nutt"esParsccontad Phontwnexens ton -Tmoer winleroconttoty erracthe exensotactpareacontsctPhonaFyttetruncatettotochare=sytwttolonosthm.th/chhre.abtetnindstodllGtru1taetbareacontnetPhondthrount.rtodesloclkoim.tthto.totowmeine.cotntrv.conparseContactMobitetestParsctontae Nonathaiddwuher - Eo numoee teturnd noek•testParseContactMobileIsTruncatedTo25Chars - Number longer than 25 chars gets truncated (Str:: 1iml•testParseContactMob1leW{thCountryCode - Local UK mobile resolved to +44. using country codeBul -1tile +80 2Accept allAsk anything (XOL)Adhotvutwdeurlatmekelwhireh2 4 space:...
|
NULL
|
-5987254938711499162
|
NULL
|
click
|
ocr
|
NULL
|
rapstomEV favscojs ~ViewCooc#12121 on JY-20963-fox rapstomEV favscojs ~ViewCooc#12121 on JY-20963-fox-hС НttpIntegrations@ interactionsJobsO MailC Modelsanoureatonsvoseivers© SocialAccountObserverTest,;C UserObserverTest.phpE UserRoleObserverTest.phpPolcies>EACtVIYActivityProviders@ CalendarTeonterenco•RullmeenClose& Copper@ HubspotListenersPipedriveSalesforce• FieidsOpportunityMatcherOpportunitySyncStrateProspectSearchStrateg& Service Traits© ClientTest.php© DecorateActivity Test.p© DeleteObjects TraitTest© FieidDefinitions Test.ph© GetActivityFieidNameT© PayloadBullder Test.phyE QueryBullder Test.php© QueryHandler Test.php©QuerviteratorTest.ohr© QueryResultsTests.phpC Semceltestoo© SyncBatchRedisService@ RaseSeniiceTest oho(el CacherCrm Senvicaßeco?3g% 9Windowwnapers.ohyTeom.one©ActivityController.phpA SF (iminny@localhost)ConsoNe (STAGING)DP000• TXAUtOvBY u.id, u.enail, u.name, u.softphone_number* fron teans where id = 1;A console (EU?045 A1 441 X66 ^Curssirvcaloslexcondsesstpublic function testFetchAndAssociateRelatedActivity(): voidSclient = Sthis->createMock( originsSpayLoadBuilder = Sthis->createMock( originalClassNgSpayLoadBuilder->nethod( constraClient::class):= PayloadBuilder::class):ScountriesMap = Sthis->createMock( origh/** Bvar ServicesMockObject SserviceMock */SserviceMock = Sthis->getMockßuilder( className: Service::class)->setConstructorArgs([Sclient.SoayLoadbuzloersevencurspaccherScountriesMap,Sthis->createMock( origina/ClassN>oncynechodstecchketacecacc.v.cy→>аскносkонwcthoekoseyocerReiaho>oneo->mnadl ci"fetchRelatedActävity")-sweeerumnia'Description' => 'Test description'.Steam = Sthis->createMock( orig)Tean: :class);"aetattobute" oythitnroums"tean"oyil Return SteamPlaybook::class);Splaybook->method(toi.af 2mdMOcLAd ~ ACCept Fle X- bX PeRet 5Mn ( 359u0DE nULU:SpLaybook-›method( constraint: 'getActivityType') -›willReturn( value: PLaybook::ACTIVITY_TYPE_EVENT):= 709710711=722733fron text_relays where created_at > *2026-85-01*)fron activities where user_id IN (7160, 18608) and created_at › '2026-05-22' order by id desc;* fron users where team_id = 1 and id IN (18608, 13934, 7160);* fron activities where user_id = 7160 order by id desc Linit 10;fron teans where id = 1117;fron activity_search_filters nhere activity_search_id IN (88882, 88902)TO0У L7Thu 28 May 16:04:25U ServiceTest ~Cascadelesorceserwiceedecker nxe dovkces/cr/5 best ree/Servicee/31.php hervendsr/orsepontact: 2261PHPUnit 11.5.55 by Sebastian Bergnann and contributors.PHP 8.S.Scontelouratciontnore/tinnyl ohounx"+0.11/ 11 (100%Tine: 00:10.908, Memory: 66.00 MBAl 11 new and existing testParseContact* tests pass. The deprecations are pre-existing PHPUnit warningsLet me also quickly run the full ServiceTest to confirm nothing is broken:o desterext deckee/er/ lestore Serviceestphg s h ventaen -2/b3n/phpunitPHPUnzt 11.5.55 by Sebastzan Beramann and contributors.Configuration: /hose/jiminny/phpunit.xmlD.•DODOD..•••D000.DD.......••• 65 / 73 ( 894Time: 00:21.744, Memory: 94.00 MBA.7s tests pass. Herc's a summary of what was added.oo eoesThe PR already covered empty/null early-return paths. The missing coverage was for the happy paths - added 7newssparseContactPhone• testParseContactPhonewithValidNVumber - E.164 number parses correctly, ext is nutt"esParsccontad Phontwnexens ton -Tmoer winleroconttoty erracthe exensotactpareacontsctPhonaFyttetruncatettotochare=sytwttolonosthm.th/chhre.abtetnindstodllGtru1taetbareacontnetPhondthrount.rtodesloclkoim.tthto.totowmeine.cotntrv.conparseContactMobitetestParsctontae Nonathaiddwuher - Eo numoee teturnd noek•testParseContactMobileIsTruncatedTo25Chars - Number longer than 25 chars gets truncated (Str:: 1iml•testParseContactMob1leW{thCountryCode - Local UK mobile resolved to +44. using country codeBul -1tile +80 2Accept allAsk anything (XOL)Adhotvutwdeurlatmekelwhireh2 4 space:...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85739
|
2938
|
8
|
2026-05-28T13:04:25.417050+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973465417_m1.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2• • 0ShellEditViewSessionScripts|ProfilesW iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKERO ₴1DEV (docker)₴82-zshN3screenpipe"app/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.phpX4348-zsh+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976+++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec-it docker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|85ec2-user@ip-10-30-129-...100% <78• Thu 28 May 16:04:241₴1ec2-user@ip-10-30-140-...₴7...
|
NULL
|
5219148681807756499
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2• • 0ShellEditViewSessionScripts|ProfilesW iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKERO ₴1DEV (docker)₴82-zshN3screenpipe"app/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.phpX4348-zsh+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976+++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec-it docker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|85ec2-user@ip-10-30-129-...100% <78• Thu 28 May 16:04:241₴1ec2-user@ip-10-30-140-...₴7...
|
85737
|
NULL
|
NULL
|
NULL
|
|
85738
|
2939
|
9
|
2026-05-28T13:04:21.319087+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973461319_m2.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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.85638297,"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":"ServiceTest","depth":6,"bounds":{"left":0.87167555,"top":0.019952115,"width":0.043882977,"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 'ServiceTest'","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 'ServiceTest'","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}]...
|
-824022620702545522
|
-9103533727753729596
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
rapstomEV favscojs ~ViewCooc#12121 on JY-20963-fox-hС НttpIntegrations@ interactionsJobsO MailC Modelsanoureatonsvoseivers© SocialAccountObserverTest,;C UserObserverTest.php€ UserRoleObserverTest.phpPolcies>EACtVIYActivityProviders@ CalendarTeonterenco•RullmeenClose# Copper@ HubspotListenersPipedriveSalesforce• FieidsOpportunityMatcherOpportunitySyncStrateProspectSearchStrateg& Service Traits© ClientTest.php© DecorateActivity Test.p© DeleteObjects TraitTest© FieidDefinitions Test.ph© GetActivityFieidNameT© PayloadBullder Test.phyE QueryBullder Test.php© QueryHandler Test.php©QuerviteratorTest.ohr© QueryResultsTests.phpC Semceltestoo© SyncBatchRedisService@ RaseSeniiceTest oho(el CacherCrm Senvicaßeco?3g% 9Windowwnapers.ohyTeom.one©ActivityController.php8000A SF (iminny@localhost)ConsoNe (STAGING)DP000• TXAUtOvBY u.id, u.enail, u.name, u.softphone_number* fron teans where id = 1;A console (EUT8458185Y00Curssercelesexendsepublic function testFetchAndAssociateRelatedActivity(): voidSclient = Sthis->createMock( origingSpayLoadBuilder = Sthis->createMock( originalClassNgSpayLoadBuilder->nethod( constraClient::class):= PayloadBuilden::class);ScountriesMap = Sthis-›createMock( crigh/** Bvar ServicesMockObject SserviceMock */SserviceMock = Sthis->getMockßuilder( className: Service::class)->setConstructorArgs(ESclient.SoayLoadbuzloersevencurspaccherScountriesMap,Sthis->createMock( origina/ClassN>oncynechodstecchketacecacc.v.cy→>аскносkонwcthoekoseyocerReiaho>oneo>mhodlfetchRelatedActivity")-sweeerumnia'Description' => 'Test description'.Steam = Sthis->createMock( originTean: :class);"aetattobute" oythitnroum"tear"oyna Return SteamPlaybook::class);Splaybook->method(toi.af2mdMOcLAd ~ ACCept Fle X- bX PeRet 5Mn ( 359u0DE nULU:SpLaybook-›method( constraint: 'qetActivityType')-›willReturn( value: Playbook::ACTIVITY_TYPE_EVENT):= 709710711=722733fron text_relays where created_at > *2026-85-01*)fron activities where user_id IN (7160, 18608) and created_at > '2026-85-22' order by id desc;* fron users where team_id = 1 and id IN (18608, 13934, 7160);* fron activities where user_id = 7160 order by id desc Linit 10;* fron teans where id = 1117:fron activity_search_filters nhere activity_search_id IN (88882, 88902)TO0У L7Thu 28 May 16:04:20ServiceTestCascadeles orchwerwicee+0.decker nxe dovkces/cr/5 best ree/Servicee/31.php hervendsr/orsepontact: 2261PHPUnit 11.5.55 by Sebastian Bergnann and contributors.PHP 8.S.Sconteouratciont no.e/tminayohounsx"11/ 11 (100%Tine: 00:10.908, Memory: 66.00 MBAl 11 new and existing testParseContact* tests pass. The deprecations are pre-existing PHPUnit warningsLet me also quickly run the full ServiceTest to confirm nothing is broken:o desterext deckee/er/ lestore Serviceestphg s h ventaen -2/b3n/phpunitPHpunit 11.5.55 by Sebastzan Beramann and contributors.Configuration: /hose/jiminny/phpunit.xmlD.•DODOD..[PASSWORD_DOTS]D000.DD[PASSWORD_DOTS]•• 65 / 73 ( 894Time: 00:21.744, Memory: 94.00 MBA.7s tests pass. Herc's a summary of what was added:oo eoesnewssThe PR already covered empty/null early-return paths. The missing coverage was for the happy paths - added 7parseContactPhone•testParseContactPhoneWithValidNumber - E.164 number parses correctly, ext is nul?"esParsccontad Phontwnexens ton -Tmoer winleroconttoty erracthe exensotactpareacontsctPhonaFyttetruncatettotochare=sytwttolonosthm.th/chhre.abtetnindstodllGtru1taetbareacontnetPhondthrount.rtodesloclkoim.tthto.totowmeine.cotntrv.conparseContactMobitetestParsctontae Nonathaiddwuher - Eo numoee teturnd noek•testParseContactMobileIsTruncatedTo25Chars - Number longer than 25 chars gets truncated (Str:: 1iml•testParseContactMob{leW{thCountryCode - Local UK mobile resolved to +44. using country code1tile +80 2Accept allAsk anything (XOL)" PodnAdhotvutwdeurlatmekelwhireh2 4 space:...
|
85736
|
NULL
|
NULL
|
NULL
|
|
85737
|
2938
|
7
|
2026-05-28T13:04:19.445089+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973459445_m1.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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":"ServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'ServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'ServiceTest'","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}]...
|
5582423643801155883
|
-8994321436789994556
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Run 'ServiceTest'
Debug 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKER881DEV (docker)₴82-zshN3screenpipe"O ₴4-zshapp/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php348+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec-it docker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|85ec2-user@ip-10-30-129-...100% <78• Thu 28 May 16:04:191₴1ec2-user@ip-10-30-140-...₴7...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85736
|
2939
|
8
|
2026-05-28T13:04:19.339867+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973459339_m2.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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.85638297,"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":"ServiceTest","depth":6,"bounds":{"left":0.87167555,"top":0.019952115,"width":0.043882977,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
9087091289234339902
|
-4023455704353896256
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
rapstomEV favscojs ~ViewCooc#12121 on JY-20963-fox-hС НttpIntegrations@ interactionsJobsO MailC Modelsanoureatonsvoseivers© SocialAccountObserverTest,;C UserObserverTest.phpE UserRoleObserverTest.phpPolcies>EACtVIYActivityProviders@ CalendarTeonterenco•RullmeenClose& Copper@ HubspotListenersPipedriveSalesforce• FieidsOpportunityMatcherOpportunitySyncStrateProspectSearchStrateg& Service Traits© ClientTest.php© DecorateActivity Test.p© DeleteObjects TraitTest© FieidDefinitions Test.ph© GetActivityFieidNameT© PayloadBullder Test.phyE QueryBullder Test.php© QueryHandler Test.php©QuerviteratorTest.ohr© QueryResultsTests.phpC Semceltestoo© SyncBatchRedisService@ RaseSeniiceTest oho(el CachedCrm Senvicaheroe)3g% 9Windowwnapers.ohyTeom.one©ActivityController.phpuCicheoneA SF (iminny@localhost)A console [STAGING)D8.Ty: Autowcreated_at > DATE_SUB(NOW(), INTERVAL 38 DAY)BY u.id, v.enail, U.name, U.softphone_number• • 0RurU ServiceTest xCG SmvOErO:~ v Test Resultsconsole [PROD] x © Service.phpA console (EUT045 A1 441 Y66 Acusesinuceleeelexendsesttpublic function testFetchAndAssociateRelatedActivity(): voidSclient = Sthis->createMock( originsSpayLoadBuilder = Sthis->createMock( originalClassNgClient::class):= PayloadBuilden::class);SpayLoadBuilder->nethod( constra->willReturnCallback(function (Sactivity, Spayload) (...H):ScountriesMap = Sthis->createMock( orighCountriesMap::class);Dispatcher: :class):=714—717E 718/** Bvar ServicesMockObject SserviceMock */SserviceMock = Sthis->getMockßuilder( className: Service: :class)->setConstructorArgs(ESclient.SoayLoadbuzloersevencurspaccherScountriesMap,Sthis->createMock( origina/ClassNProspectPhotoPathService::class),i gerltybokcatogoryi , ptsteifecere ) m>oncynechodstecchketacecacc.vzcv→>аскносkон=wcthoekoseyocerReiaho>oneo->mnadl cifetchRelatedActivity")-sweeerumnia=733'Description' => 'Test description'.Steam = Sthis-›createMock( originTean: :class);"aetattoibute"oythltearoum"tean"oyil Return SteamINEEERO&SPlaybook::class);Splaybook->method(toi.af 2mdMOcLAd ~ ACCept Fle X- bX PeRet 5Mn ( 359u0DE nULU:SpLaybook-›method( constraint: 'getActivityType') -›willReturn( value: PLaybook::ACTIVITY_TYPE_EVENT):v 73 tests passed 73 tests total, 2 sec 290 ms[docker-compose://[/Users/Lukas/jiminny/infrastructure/dev/docker/docker-compose.ymU):Zamp/]:php ./vendor/bin/phpunit --configuration phpunit.xml --filter Tests|\Unit||Servicesl\Testing started at 16:04 ….WARN[6000] /Users/lukas/jiminny/infrastructure/dev/docker/docker-compose.ynl: the attribute "version' is obsolete, it will be ignored, please renove it to avoid potential confusiPHPUnst 11.5.55 by Sebastion Bergnann and contributors.PuntindePHp 8.5,SConfiguration: /hone/jininny/phpunit.xmlsino: 88:89-866. Menory: 94.00M:There was 1 PHPUnit test runner warning:1) No code coverage driver availableOK, but there were issues!Tests: 73, Assertions: 300, PHPUnit Warnängs: 1, PHPUnit Deprecations: 17.What's next:Debug this Compose error with Gordon » docker ai "help me fix this compose erron"Process finished with exit code 1*Tron licand whene nacd iuke lSuhnetemgti*Thor tasne whendr14* fron activity_searches where user_id = 31854:*Eoon sctkusty coanch Eilitane mhans actkusty canoch 4a TM CRRRR2 RRO02< 50 MNoo Inu comoy 1o:04.1eServiceTestCascadeles orcesirwich+0.deckeunxes deckees/cr/3 best ree/Servicer/31.php -, Ptervenesr/orsepontact: 2561PHPUnit 11.5.55 by Sebastian Bergnann and contributors.11/ 11 (100%2 sec 290 ms1tile +80 2Ask anything (XOL)c HodeAdhotv-existing PHPUnit warnings-=/bin/phpunst..% / 73 (1804)s for the happy paths - added 7y, ext is nutletacehe exensotan 10 chars gets truncated (Str::tMueine countrcon5 chars gets truncated (Str:: lint444. using country codeB ul -RT RetetalAcceot allIModeuralaimeehies*4 space...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85735
|
2939
|
7
|
2026-05-28T13:04:04.730564+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973444730_m2.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Rerun 'PHPUnit: ServiceTest'
Debug 'ServiceTest'
Stop 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Built-in Preview
Chrome
Firefox
Safari
Sync Changes
Hide This Notification
Code changed:
Hide
3
32
81
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Salesforce;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Facades\Cache;
use Jiminny\Component\Country\CountriesMap;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Enums\CrmObject;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\NoResultsException;
use Jiminny\Integrations\PlaybookResolver;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Playbook;
use Jiminny\Models\Team;
use Jiminny\Models\TeamSettings;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Salesforce\Client;
use Jiminny\Services\Crm\Salesforce\PayloadBuilder;
use Jiminny\Services\Crm\Salesforce\QueryBuilder;
use Jiminny\Services\Crm\Salesforce\QueryHandler;
use Jiminny\Services\Crm\Salesforce\QueryIterator;
use Jiminny\Services\Crm\Salesforce\QueryResults;
use Jiminny\Services\Crm\Salesforce\Service;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
use Tests\Unit\Traits\TestPrivateMethod;
class ServiceTest extends TestCase
{
use TestPrivateMethod;
public function testFetchAndAssociateRelatedActivity(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$payloadBuilder->method('addCustomLogicFieldsPayload')
->willReturnCallback(function ($activity, $payload) {
return $payload;
});
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods(['fetchRelatedActivity', 'getPlaybook', 'getPlaybookCategory', 'updateRecord'])
->getMock();
$serviceMock->expects($this->once())
->method('fetchRelatedActivity')
->willReturn([
'Id' => 'testId',
'Type' => null,
'OwnerId' => 'testerUser',
'Description' => 'Test description',
]);
$user = $this->createMock(User::class);
$team = $this->createMock(Team::class);
$user->method('getAttribute')->with('team')->willReturn($team);
$playbook = $this->createMock(Playbook::class);
$playbook->method('getActivityField')->willReturn(null);
$playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_EVENT);
$serviceMock->expects($this->once())
->method('getPlaybook')
->with($user)
->willReturn($playbook);
$serviceMock->expects($this->never())
->method('getPlaybookCategory');
$serviceMock->expects($this->never())
->method('updateRecord');
$fieldDataRepository = $this->createMock(FieldDataRepository::class);
$fieldDataRepository->method('getActivityFieldData')->willReturn(collect([]));
app()->instance(FieldDataRepository::class, $fieldDataRepository);
$config = $this->createMock(Configuration::class);
$profilesRelation = $this->getMockBuilder(\Illuminate\Database\Eloquent\Relations\HasMany::class)
->disableOriginalConstructor()
->onlyMethods(['get'])
->addMethods(['where'])
->getMock();
$profilesRelation->method('where')->willReturnSelf();
$profilesRelation->method('get')->willReturn(collect([]));
$config->method('profiles')->willReturn($profilesRelation);
$serviceMock->config = $config;
$serviceMock->profile = null;
$actualStartTime = \Carbon\Carbon::now();
$activity = $this->getMockBuilder(Activity::class)
->disableOriginalConstructor()
->onlyMethods(['update', 'hasProspect'])
->getMock();
$activity->method('update')->willReturn(true);
$activity->method('hasProspect')->willReturn(true);
$activity->type = Activity::TYPE_CONFERENCE;
$activity->provider = Activity::PROVIDER_TWILIO;
$activity->lead_id = 1;
$activity->user_id = 0;
$activity->id_string = 'test-activity-id';
$activity->user = $user;
$activity->actual_start_time = $actualStartTime;
$activity->uuid = 'c53d8320-f556-4cee-a2f8-5f232f454ca4';
app()->bind(PlaybookResolver::class, function () use ($user) {
$playbook = $this->createMock(Playbook::class);
$playbookResolver = $this->createMock(PlaybookResolver::class);
$playbookResolver->expects($this->once())
->method('resolvePlaybookByUser')
->with($user)
->willReturn($playbook);
return $playbookResolver;
});
$data = $serviceMock->fetchAndAssociateRelatedActivity($activity);
$this->assertInstanceOf(Activity::class, $data);
$this->assertEquals(Activity::TYPE_CONFERENCE, $data->getType());
$this->assertEquals($actualStartTime->getTimestamp(), $data->getActualStartTime()->getTimestamp());
}
public function testFetchAndAssociateRelatedActivitySkipsForTaskBasedPlaybook(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods(['fetchRelatedActivity', 'getPlaybook'])
->getMock();
$user = $this->createMock(User::class);
$playbook = $this->createMock(Playbook::class);
$playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);
$playbook->method('getId')->willReturn(123);
$serviceMock->expects($this->once())
->method('getPlaybook')
->with($user)
->willReturn($playbook);
$serviceMock->expects($this->never())
->method('fetchRelatedActivity');
$activity = $this->getMockBuilder(Activity::class)
->disableOriginalConstructor()
->onlyMethods(['hasProspect', 'getUuid'])
->getMock();
$activity->method('hasProspect')->willReturn(true);
$activity->method('getUuid')->willReturn('c53d8320-f556-4cee-a2f8-5f232f454ca4');
$activity->type = Activity::TYPE_CONFERENCE;
$activity->actual_start_time = \Carbon\Carbon::now();
$activity->user = $user;
$result = $serviceMock->fetchAndAssociateRelatedActivity($activity);
$this->assertNull($result);
}
public function testFetchAndAssociateRelatedActivityReturnsNullForNonConference(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
$serviceMock = new Service(
client: $client,
payloadBuilder: $payloadBuilder,
eventDispatcher: $eventDispatcher,
countriesMap: $countriesMap,
prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class)
);
$activity = $this->getMockBuilder(Activity::class)
->disableOriginalConstructor()
->getMock();
$activity->type = Activity::TYPE_SOFTPHONE;
$result = $serviceMock->fetchAndAssociateRelatedActivity($activity);
$this->assertNull($result);
}
public function testFetchAndAssociateRelatedActivityReturnsNullWhenNoStartTime(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
$serviceMock = new Service(
client: $client,
payloadBuilder: $payloadBuilder,
eventDispatcher: $eventDispatcher,
countriesMap: $countriesMap,
prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class)
);
$activity = $this->getMockBuilder(Activity::class)
->disableOriginalConstructor()
->getMock();
$activity->type = Activity::TYPE_CONFERENCE;
$activity->actual_start_time = null;
$activity->scheduled_start_time = null;
$result = $serviceMock->fetchAndAssociateRelatedActivity($activity);
$this->assertNull($result);
}
public function testFetchAndAssociateRelatedActivityReturnsNullWhenNoProspect(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods(['getPlaybook'])
->getMock();
$serviceMock->expects($this->never())
->method('getPlaybook');
$activity = $this->getMockBuilder(Activity::class)
->disableOriginalConstructor()
->onlyMethods(['hasProspect', 'getUuid'])
->getMock();
$activity->method('hasProspect')->willReturn(false);
$activity->method('getUuid')->willReturn('c53d8320-f556-4cee-a2f8-5f232f454ca4');
$activity->type = Activity::TYPE_CONFERENCE;
$activity->actual_start_time = \Carbon\Carbon::now();
$result = $serviceMock->fetchAndAssociateRelatedActivity($activity);
$this->assertNull($result);
}
public function testMatchExactlyByEmail(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods([])
->getMock();
$profile = new Profile();
$profile->setAttribute('id', bin2hex(random_bytes(8)));
$serviceMock->profile = $profile;
$team = $this->createMock(Team::class);
$serviceMock->team = $team;
$data = $serviceMock->matchExactlyByEmail(bin2hex(random_bytes(8)) . '[EMAIL]');
$this->assertEquals(null, $data);
}
public function testMatchDomainFromEmail(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$queryIterator = $this->createMock(QueryIterator::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
$config = $this->createMock(Configuration::class);
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->any())
->method('search')
->willReturn($queryIterator);
return $handler;
});
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods(['convertCrmData'])
->getMock();
$profile = new Profile();
$profile->account_fields = 'Field1, Field2, Field3';
$serviceMock->profile = $profile;
$serviceMock->expects($this->once())
->method('convertCrmData')
->willReturn(['test']);
$this->app->bind(QueryBuilder::class, function () {
$queryBuilder = $this->createMock(QueryBuilder::class);
$queryBuilder->expects($this->once())
->method('buildMatchByDomainQuery')
->with('[EMAIL]')
->willReturn('FIND {[EMAIL]} IN ALL FIELDS RETURNING Account(Id)');
return $queryBuilder;
});
$team = $this->createMock(Team::class);
$serviceMock->team = $team;
$serviceMock->config = $config;
$data = $serviceMock->matchByDomain('[EMAIL]');
$this->assertEquals(['test'], $data);
}
public function testBuildTaskSearchFields(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
$service = new Service(
client: $client,
payloadBuilder: $payloadBuilder,
eventDispatcher: $eventDispatcher,
countriesMap: $countriesMap,
prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class)
);
$fields = $service->buildTaskSearchFields();
$expectedFields = ['Id', 'WhoId', 'WhatId', 'AccountId'];
$this->assertEquals($expectedFields, $fields);
}
public function testMapCrmObjects(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
$service = new Service(
client: $client,
payloadBuilder: $payloadBuilder,
eventDispatcher: $eventDispatcher,
countriesMap: $countriesMap,
prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class),
);
$sampleTask = [
'WhoId' => '003sampleWhoId',
'AccountId' => 'sampleAccountId',
'WhatId' => 'sampleWhatId',
];
$activityData = $service->mapCrmObjects($sampleTask);
$expectedActivityData = [
'contact' => '003sampleWhoId',
'account' => 'sampleAccountId',
'opportunity' => 'sampleWhatId',
];
$this->assertEquals($expectedActivityData, $activityData);
}
public function testGetInstalledAppVersion(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
$queryIterator = $this->createMock(QueryIterator::class);
$queryIterator->expects($this->any())
->method('current')->willReturn([
'SubscriberPackageVersion' => [
'MajorVersion' => '1',
'MinorVersion' => '0',
'PatchVersion' => '1',
'BuildNumber' => '0',
],
]);
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->any())
->method('metadata')
->willReturn($queryIterator);
return $handler;
});
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods(array_diff(get_class_methods(Service::class), ['getInstalledAppVersion']))
->getMock();
$version = $serviceMock->getInstalledAppVersion();
$this->assertEquals('1010', $version);
}
public function testSyncProfiles(): void
{
$userToSearch = null;
$team = $this->createMock(Team::class);
$config = $this->createMockedConfiguration();
$config->expects($this->once())
->method('getId')
->willReturn(1);
$salesforceUser = [
'Email' => '[EMAIL]',
'UserPreferencesLightningExperiencePreferred' => true,
'CallCenterId' => '123',
'Id' => '456',
'ProfileId' => '789',
];
app()->bind(QueryBuilder::class, function () use ($userToSearch) {
$queryBuilder = $this->createMock(QueryBuilder::class);
$queryBuilder->expects($this->once())
->method('buildGetUsersQuery')
->with($userToSearch)
->willReturn('SELECT * FROM Users');
return $queryBuilder;
});
$queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults([$salesforceUser], 1, true, null));
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->any())
->method('query')
->with('SELECT * FROM Users')
->willReturn($queryIterator);
return $handler;
});
$user = $this->createMock(User::class);
$user->expects($this->once())
->method('getId')
->willReturn(123);
$this->mockTeamRepository($team, $salesforceUser, $user);
$profileRepository = $this->createMock(ProfileRepository::class);
$profileRepository->expects($this->once())
->method('updateOrCreateProfile')
->with(
$user,
[
'crm_configuration_id' => 1,
'crm_provider_id' => '456',
],
[
'user_id' => 123,
'edition' => Profile::EDITION_LIGHTNING,
'has_external_cti' => true,
'crm_profile_id' => '789',
]
)
->willReturn(new Profile());
$this->app->instance(ProfileRepository::class, $profileRepository);
$serviceMock = $this->getServiceMock();
$serviceMock->team = $team;
$serviceMock->config = $config;
$result = $serviceMock->syncProfiles($userToSearch);
$this->assertNull($result);
}
public function testSyncProfilesEmailIsNull(): void
{
$userToSearch = $this->createMock(User::class);
$salesforceUser = [
'Email' => null,
];
app()->bind(QueryBuilder::class, function () {
$queryBuilder = $this->createMock(QueryBuilder::class);
$queryBuilder->expects($this->once())
->method('buildGetUsersQuery')
->with(null)
->willReturn('SELECT * FROM Users');
return $queryBuilder;
});
$queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults([$salesforceUser], 1, true, null));
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->exactly(2))
->method('query')
->willReturn($queryIterator);
return $handler;
});
$team = $this->createMock(Team::class);
$user = $this->createMock(User::class);
$this->mockTeamRepository($team, $salesforceUser, $user, false);
$config = $this->createMock(Configuration::class);
$serviceMock = $this->getServiceMock();
$serviceMock->team = $team;
$serviceMock->config = $config;
$profile = $serviceMock->syncProfiles(null);
$this->assertNull($profile);
}
public function testSyncProfilesUserToSearchMatchesCurrentUser(): void
{
$userToSearch = $this->createMock(User::class);
$userToSearch->expects($this->once())
->method('getId')
->willReturn(123);
$team = $this->createMock(Team::class);
$config = $this->createMock(Configuration::class);
$config->expects($this->once())
->method('getId')
->willReturn(1);
$salesforceUser = [
'Email' => '[EMAIL]',
'UserPreferencesLightningExperiencePreferred' => true,
'CallCenterId' => '123',
'Id' => '456',
'ProfileId' => '789',
];
app()->bind(QueryBuilder::class, function () use ($userToSearch) {
$queryBuilder = $this->createMock(QueryBuilder::class);
$queryBuilder->expects($this->once())
->method('buildGetUsersQuery')
->with($userToSearch)
->willReturn('SELECT * FROM Users');
return $queryBuilder;
});
$queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults([$salesforceUser], 1, true, null));
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->any())
->method('query')
->with('SELECT * FROM Users')
->willReturn($queryIterator);
return $handler;
});
$user = $this->createMock(User::class);
$user->expects($this->exactly(2))
->method('getId')
->willReturn(123);
$this->mockTeamRepository($team, $salesforceUser, $user);
$profileRepository = $this->createMock(ProfileRepository::class);
$profileRepository->expects($this->once())
->method('updateOrCreateProfile')
->with(
$user,
[
'crm_configuration_id' => 1,
'crm_provider_id' => '456',
],
[
'user_id' => 123,
'edition' => Profile::EDITION_LIGHTNING,
'has_external_cti' => true,
'crm_profile_id' => '789',
]
)
->willReturn(new Profile());
$this->app->instance(ProfileRepository::class, $profileRepository);
$serviceMock = $this->getServiceMock();
$serviceMock->team = $team;
$serviceMock->config = $config;
$profile = $serviceMock->syncProfiles($userToSearch);
$this->assertInstanceOf(Profile::class, $profile);
}
public function testSyncProfilesWithCustomValidation(): void
{
$userToSearch = null;
$team = $this->createMock(Team::class);
$config = $this->createMockedConfiguration();
$config->expects($this->atLeastOnce()) // Changed from once() to atLeastOnce()
->method('getId')
->willReturn(1);
$salesforceUser = [
'Email' => '[EMAIL]',
'UserPreferencesLightningExperiencePreferred' => true,
'CallCenterId' => '123',
'Id' => '456',
'ProfileId' => '789',
'CustomField' => 'CustomValue',
];
$customRules = [
['field' => 'CustomField', 'value' => 'CustomValue'],
];
$this->mockQueryBuilderAndHandler($userToSearch, [$salesforceUser]);
$user = $this->createMock(User::class);
$user->expects($this->once())
->method('getId')
->willReturn(123);
$this->mockTeamRepository($team, $salesforceUser, $user, true, $customRules);
$this->mockProfileRepository($user);
$serviceMock = $this->getServiceMock();
$serviceMock->team = $team;
$serviceMock->config = $config;
$profile = $serviceMock->syncProfiles($userToSearch);
$this->assertNull($profile);
}
public function testSyncProfilesWithCustomValidationFailing(): void
{
$userToSearch = null;
$team = $this->createMock(Team::class);
$config = $this->createMockedConfiguration();
$salesforceUser = [
'Email' => '[EMAIL]',
'UserPreferencesLightningExperiencePreferred' => true,
'CallCenterId' => '123',
'Id' => '456',
'ProfileId' => '789',
'CustomField' => 'WrongValue',
];
$customRules = [
['field' => 'CustomField', 'value' => 'CustomValue'],
];
$this->mockQueryBuilderAndHandler($userToSearch, [$salesforceUser]);
$teamRepository = $this->getMockForAbstractClass(TeamRepository::class, [], '', false, true, true, ['findActiveTeamMemberByEmail', 'getTeamSetting']);
$teamSettings = $this->createMock(TeamSettings::class);
$teamSettings->method('getValueType')
->willReturn('array');
$teamSettings->method('getValue')
->willReturn(json_encode($customRules));
$teamRepository->expects($this->once())
->method('getTeamSetting')
->with($team, 'custom_profile_validation')
->willReturn($teamSettings);
app()->bind(TeamRepository::class, function () use ($teamRepository) {
return $teamRepository;
});
$profileRepository = $this->createMock(ProfileRepository::class);
$profileRepository->expects($this->never())
->method('updateOrCreateProfile');
$this->app->instance(ProfileRepository::class, $profileRepository);
$serviceMock = $this->getServiceMock();
$serviceMock->team = $team;
$serviceMock->config = $config;
$result = $serviceMock->syncProfiles($userToSearch);
$this->assertNull($result);
}
public function testGetContactRolesFromCrm(): void
{
$contactRoles = [
[
'Id' => '1',
'ContactId' => 'Contact1',
'OpportunityId' => 'Opportunity1',
'Opportunity' => ['OwnerId' => 'Owner1'],
'IsPrimary' => true,
'Role' => 'Decision Maker',
],
];
$expectedResponse = [
[
'id' => '1',
'contactId' => 'Contact1',
'opportunityId' => 'Opportunity1',
'ownerId' => 'Owner1',
'isPrimary' => true,
'role' => 'Decision Maker',
],
];
$this->bindQueryIterator($contactRoles);
$serviceMock = $this->getServiceMock();
$data = $serviceMock->getContactRolesFromCrm(now()->subDay());
$this->assertEquals($expectedResponse, $data);
}
public function testGetContactRolesFromCrmNoResult(): void
{
$this->bindQueryIterator([]);
$serviceMock = $this->getServiceMock();
$data = $serviceMock->getContactRolesFromCrm(now()->subDay());
$this->assertEquals([], $data);
}
public function testSyncContactRoles(): void
{
$contactRoles = [
[
'id' => '1',
'contactId' => 'Contact1',
'opportunityId' => 'Opportunity1',
'ownerId' => 'Owner1',
'isPrimary' => true,
'role' => 'Decision Maker',
],
];
app()->bind(ContactRoleRepository::class, function () {
$contactRoleRepository = $this->createMock(ContactRoleRepository::class);
$contactRoleRepository->expects($this->once())
->method('saveContactRoles');
return $contactRoleRepository;
});
$serviceMock = $this->getServiceMock([
'getContactRolesFromCrm',
'syncRemotelyDeletedContactRoles',
'syncContact',
'syncOpportunity',
]);
$config = $this->createMock(Configuration::class);
$hasMany = $this->createMock(HasManyExtended::class);
$hasMany->expects($this->exactly(2))
->method('where')
->willReturn($hasMany);
$hasMany->expects($this->exactly(2))
->method('first')
->willReturn(
$this->createMock(Contact::class),
$this->createMock(Opportunity::class)
);
$config->expects($this->once())
->method('contacts')
->willReturn($hasMany);
$config->expects($this->once())
->method('opportunities')
->willReturn($hasMany);
$serviceMock->config = $config;
$serviceMock->expects($this->once())
->method('getContactRolesFromCrm')
->willReturn($contactRoles);
$serviceMock->expects($this->once())
->method('syncRemotelyDeletedContactRoles');
$data = $serviceMock->syncContactRoles(now()->subDay());
$this->assertEquals(1, $data);
}
public function testSyncRemotelyDeletedContactRoles(): void
{
$contactRoles = [
[
'id' => '1',
'crm_provider_id' => '1',
],
];
app()->bind(QueryHandler::class, function () use ($contactRoles) {
$queryResults = new QueryResults($contactRoles, 1, true, null);
$handler = $this->createMock(QueryHandler::class);
$handler->method('queryDeleted')
->willReturn($queryResults);
return $handler;
});
app()->bind(ContactRoleRepository::class, function () {
$contactRoleRepository = $this->createMock(ContactRoleRepository::class);
$contactRoleRepository->expects($this->once())
->method('deleteContactRoles');
return $contactRoleRepository;
});
$serviceMock = $this->getServiceMock();
$serviceMock->team = $this->createMock(Team::class);
$data = $this->invokePrivateMethod('syncRemotelyDeletedContactRoles', $serviceMock, []);
$this->assertTrue($data);
}
private function bindQueryIterator(array $queryResult): void
{
/** @var Client $client */
$client = $this->createMock(Client::class);
$queryIterator = new QueryIterator(
$client,
new QueryResults($queryResult, 1, true, null)
);
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->any())
->method('query')
->willReturn($queryIterator);
return $handler;
});
}
public static function getOpportunitySortOrderDataProvider(): array
{
return [
'all open recently updated' => [Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED, ['LastModifiedDate DESC', true]],
'all open recently created' => [Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED, ['CreatedDate DESC', true]],
'all open oldest created' => [Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED, ['CreatedDate ASC', true]],
'all recently updated' => [Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED, ['LastModifiedDate DESC', false]],
'default' => ['unknown', ['LastModifiedDate DESC', true]],
];
}
private function createMockedConfiguration(): Configuration
{
$config = $this->createMock(Configuration::class);
$profilesRelation = $this->getMockBuilder(\Illuminate\Database\Eloquent\Relations\HasMany::class)
->disableOriginalConstructor()
->onlyMethods(['get'])
->addMethods(['where', 'first'])
->getMock();
$profilesRelation->method('where')->willReturnSelf();
$profilesRelation->method('get')->willReturn(collect([]));
$profilesRelation->method('first')->willReturn(null);
$config->method('profiles')->willReturn($profilesRelation);
return $config;
}
private function getServiceMock(array $onlyMethods = []): MockObject&Service
{
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$this->createMock(Client::class),
$this->createMock(PayloadBuilder::class),
$this->createMock(Dispatcher::class),
$this->createMock(CountriesMap::class),
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods($onlyMethods)
->getMock();
$serviceMock->profile = $this->createMock(Profile::class);
return $serviceMock;
}
private function mockQueryBuilderAndHandler($userToSearch, $salesforceUsers): void
{
app()->bind(QueryBuilder::class, function () use ($userToSearch) {
$queryBuilder = $this->createMock(QueryBuilder::class);
$queryBuilder->expects($this->once())
->method('buildGetUsersQuery')
->with($userToSearch)
->willReturn('SELECT * FROM Users');
return $queryBuilder;
});
$queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults($salesforceUsers, count($salesforceUsers), true, null));
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->any())
->method('query')
->willReturn($queryIterator);
return $handler;
});
}
private function mockTeamRepository(
Team $team,
array $salesforceUser,
?User $user = null,
bool $userSearch = true,
array $customRules = []
): void {
$teamRepository = $this->getMockForAbstractClass(TeamRepository::class, [], '', false, true, true, ['findActiveTeamMemberByEmail', 'getTeamSetting']);
if ($userSearch) {
$teamRepository->expects($this->once())
->method('findActiveTeamMemberByEmail')
->with($team, $salesforceUser['Email'])
->willReturn($user);
}
$teamSettings = $this->createMock(TeamSettings::class);
$teamSettings->method('getValueType')
->willReturn('array');
$teamSettings->method('getValue')
->willReturn(json_encode($customRules));
$teamRepository->expects($this->once())
->method('getTeamSetting')
->with($team, 'custom_profile_validation')
->willReturn($teamSettings);
app()->bind(TeamRepository::class, function () use ($teamRepository) {
return $teamRepository;
});
}
private function mockProfileRepository(User $user): void
{
$profileRepository = $this->createMock(ProfileRepository::class);
$profileRepository->expects($this->once())
->method('updateOrCreateProfile')
->with(
$user,
[
'crm_configuration_id' => 1,
'crm_provider_id' => '456',
],
[
'user_id' => 123,
'edition' => Profile::EDITION_LIGHTNING,
'has_external_cti' => true,
'crm_profile_id' => '789',
]
)
->willReturn(new Profile());
$this->app->instance(ProfileRepository::class, $profileRepository);
}
public function testBuildEnhancedNoteDecodesHtmlEntities(): void
{
$service = $this->getServiceMock(['createRecord']);
$profile = new Profile();
$profile->setAttribute('crm_provider_id', 'owner-123');
$service->profile = $profile;
$service->expects($this->exactly(2))
->method('createRecord')
->willReturnOnConsecutiveCalls('note-id-123', 'link-id-456');
$bodyWithEntities = 'Welch's current challenges and Facebook's Club';
$result = $this->invokePrivateMethod('buildEnhancedNote', $service, [
'Test Title',
$bodyWithEntities,
'object-id-789',
]);
$this->assertEquals('note-id-123', $result);
}
public function testBuildEnhancedNoteSanitizesWithoutQuotes(): void
{
$service = $this->getServiceMock(['createRecord']);
$profile = new Profile();
$profile->setAttribute('crm_provider_id', 'owner-456');
$service->profile = $profile;
$service->expects($this->exactly(2))
->method('createRecord')
->willReturnCallback(function ($type, $data) {
if ($type === 'ContentNote') {
$decoded = base64_decode($data['Content']);
$this->assertStringContainsString("Welch's", $decoded);
$this->assertStringNotContainsString(''', $decoded);
$this->assertStringNotContainsString('&#039;', $decoded);
$this->assertStringContainsString('<script>', $decoded);
return 'note-id-456';
}
return 'link-id-789';
});
$bodyWithMixedContent = "Welch's and <script>alert('xss')</script>";
$result = $this->invokePrivateMethod('buildEnhancedNote', $service, [
'Test Title',
$bodyWithMixedContent,
'object-id-123',
]);
$this->assertEquals('note-id-456', $result);
}
public function testBuildEnhancedNoteConvertsLineBreaks(): void
{
$service = $this->getServiceMock(['createRecord']);
$profile = new Profile();
$profile->setAttribute('crm_provider_id', 'owner-789');
$service->profile = $profile;
$service->expects($this->exactly(2))
->method('createRecord')
->willReturnCallback(function ($type, $data) {
if ($type === 'ContentNote') {
$decoded = base64_decode($data['Content']);
$this->assertStringContainsString('<br>', $decoded);
$this->assertStringNotContainsString('<br />', $decoded);
return 'note-id-789';
}
return 'link-id-012';
});
$bodyWithLineBreaks = "Line 1\nLine 2\nLine 3";
$result = $this->invokePrivateMethod('buildEnhancedNote', $service, [
'Test Title',
$bodyWithLineBreaks,
'object-id-456',
]);
$this->assertEquals('note-id-789', $result);
}
public function testBuildEnhancedNoteHandlesComplexScenario(): void
{
$service = $this->getServiceMock(['createRecord']);
$profile = new Profile();
$profile->setAttribute('crm_provider_id', 'owner-complex');
$service->profile = $profile;
$service->expects($this->exactly(2))
->method('createRecord')
->willReturnCallback(function ($type, $data) {
if ($type === 'ContentNote') {
$decoded = base64_decode($data['Content']);
$this->assertStringContainsString("Welch's", $decoded);
$this->assertStringContainsString("Facebook's Club", $decoded);
$this->assertStringContainsString("Arctics'", $decoded);
$this->assertStringNotContainsString(''', $decoded);
$this->assertStringNotContainsString('&#039;', $decoded);
$this->assertStringContainsString('<br>', $decoded);
$this->assertStringContainsString('<', $decoded);
$this->assertStringContainsString('>', $decoded);
return 'note-complex';
}
return 'link-complex';
});
$complexBody = "Summary:\n---------\nThe call focused on understanding Welch's current challenges and exploring how Arctics' Revenue Growth Management solutions could support their strategic goals.\n\n• John SMith discussed his role as a category advisor for Google and Facebook's Club, emphasizing the importance of market research and advising on product assortment.\n• Madona introduced Arctics' Virtual Shoppers AI, which simulates consumer <behavior> to optimize pricing and promotional strategies.";
$result = $this->invokePrivateMethod('buildEnhancedNote', $service, [
'Jiminny Transcription Summary',
$complexBody,
'task-id-001',
]);
$this->assertEquals('note-complex', $result);
}
public function testSyncRemotelyDeletedObjectsWithErrorHandlingSuccess(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$prospectPhotoPathService,
])
->onlyMethods(['syncRemotelyDeletedObjects'])
->getMock();
// Mock team
$team = $this->createMock(Team::class);
$team->method('getUuid')->willReturn('team-uuid-123');
$serviceMock->team = $team;
// Expect syncRemotelyDeletedObjects to be called once and succeed
$serviceMock->expects($this->once())
->method('syncRemotelyDeletedObjects')
->with(\Jiminny\Enums\CrmObject::ACCOUNT);
// Call the protected method using reflection
$reflection = new \ReflectionClass($serviceMock);
$method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');
$method->setAccessible(true);
// Should not throw any exceptions
$method->invoke($serviceMock, \Jiminny\Enums\CrmObject::ACCOUNT);
$this->assertTrue(true); // Test completed successfully
}
public function testSyncRemotelyDeletedObjectsWithErrorHandlingFailure(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$prospectPhotoPathService,
])
->onlyMethods(['syncRemotelyDeletedObjects'])
->getMock();
// Mock team
$team = $this->createMock(Team::class);
$team->method('getUuid')->willReturn('team-uuid-456');
$serviceMock->team = $team;
// Mock logger to verify warning is logged
$logger = $this->createMock(\Psr\Log\LoggerInterface::class);
// Use reflection to set the protected logger property
$reflection = new \ReflectionClass($serviceMock);
$loggerProperty = $reflection->getProperty('logger');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($serviceMock, $logger);
$exception = new \Exception('Sync failed due to API error');
// Expect syncRemotelyDeletedObjects to throw an exception
$serviceMock->expects($this->once())
->method('syncRemotelyDeletedObjects')
->with(\Jiminny\Enums\CrmObject::CONTACT)
->willThrowException($exception);
// Expect warning to be logged with correct message and parameters
$logger->expects($this->once())
->method('warning')
->with(
'[Salesforce] Remotely deleted objects sync failed',
[
'objectType' => 'contact',
'teamId' => 'team-uuid-456',
'reason' => 'Sync failed due to API error',
]
);
// Call the protected method using reflection
$reflection = new \ReflectionClass($serviceMock);
$method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');
$method->setAccessible(true);
// Should not re-throw the exception, just log it
$method->invoke($serviceMock, \Jiminny\Enums\CrmObject::CONTACT);
$this->assertTrue(true); // Test completed successfully
}
public function testSyncRemotelyDeletedObjectsWithErrorHandlingWithLogParams(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$prospectPhotoPathService,
])
->onlyMethods(['syncRemotelyDeletedObjects'])
->getMock();
// Mock team
$team = $this->createMock(Team::class);
$team->method('getUuid')->willReturn('team-uuid-789');
$serviceMock->team = $team;
// Mock logger to verify warning is logged
$logger = $this->createMock(\Psr\Log\LoggerInterface::class);
// Use reflection to set the protected logger property
$loggerReflection = new \ReflectionClass($serviceMock);
$loggerProperty = $loggerReflection->getProperty('logger');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($serviceMock, $logger);
$exception = new \Exception('Network timeout');
// Expect syncRemotelyDeletedObjects to throw an exception
$serviceMock->expects($this->once())
->method('syncRemotelyDeletedObjects')
->with(\Jiminny\Enums\CrmObject::OPPORTUNITY)
->willThrowException($exception);
// Additional log parameters
$logParams = [
'syncType' => 'full',
'batchSize' => 100,
];
// Expect warning to be logged with merged parameters
$logger->expects($this->once())
->method('warning')
->with(
'[Salesforce] Remotely deleted objects sync failed',
[
'objectType' => 'opportunity',
'teamId' => 'team-uuid-789',
'reason' => 'Network timeout',
'syncType' => 'full',
'batchSize' => 100,
]
);
// Call the protected method using reflection
$reflection = new \ReflectionClass($serviceMock);
$method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');
...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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.85638297,"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":"ServiceTest","depth":6,"bounds":{"left":0.87167555,"top":0.019952115,"width":0.043882977,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rerun 'PHPUnit: ServiceTest'","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 'ServiceTest'","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":"Stop 'ServiceTest'","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":"More Actions","depth":6,"bounds":{"left":0.9494681,"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":"Built-in Preview","depth":4,"bounds":{"left":0.35904256,"top":0.15562649,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chrome","depth":4,"bounds":{"left":0.36768618,"top":0.15562649,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Firefox","depth":4,"bounds":{"left":0.37632978,"top":0.15562649,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Safari","depth":4,"bounds":{"left":0.3849734,"top":0.15562649,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"32","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"81","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.009640957,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"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.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Crm\\Salesforce;\n\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Events\\Dispatcher;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Jiminny\\Component\\Country\\CountriesMap;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Enums\\CrmObject;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\NoResultsException;\nuse Jiminny\\Integrations\\PlaybookResolver;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\TeamSettings;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Salesforce\\Client;\nuse Jiminny\\Services\\Crm\\Salesforce\\PayloadBuilder;\nuse Jiminny\\Services\\Crm\\Salesforce\\QueryBuilder;\nuse Jiminny\\Services\\Crm\\Salesforce\\QueryHandler;\nuse Jiminny\\Services\\Crm\\Salesforce\\QueryIterator;\nuse Jiminny\\Services\\Crm\\Salesforce\\QueryResults;\nuse Jiminny\\Services\\Crm\\Salesforce\\Service;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\nuse Tests\\Unit\\Traits\\TestPrivateMethod;\n\nclass ServiceTest extends TestCase\n{\n use TestPrivateMethod;\n\n public function testFetchAndAssociateRelatedActivity(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $payloadBuilder->method('addCustomLogicFieldsPayload')\n ->willReturnCallback(function ($activity, $payload) {\n return $payload;\n });\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods(['fetchRelatedActivity', 'getPlaybook', 'getPlaybookCategory', 'updateRecord'])\n ->getMock();\n\n $serviceMock->expects($this->once())\n ->method('fetchRelatedActivity')\n ->willReturn([\n 'Id' => 'testId',\n 'Type' => null,\n 'OwnerId' => 'testerUser',\n 'Description' => 'Test description',\n ]);\n\n $user = $this->createMock(User::class);\n $team = $this->createMock(Team::class);\n $user->method('getAttribute')->with('team')->willReturn($team);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityField')->willReturn(null);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_EVENT);\n\n $serviceMock->expects($this->once())\n ->method('getPlaybook')\n ->with($user)\n ->willReturn($playbook);\n\n $serviceMock->expects($this->never())\n ->method('getPlaybookCategory');\n\n $serviceMock->expects($this->never())\n ->method('updateRecord');\n\n $fieldDataRepository = $this->createMock(FieldDataRepository::class);\n $fieldDataRepository->method('getActivityFieldData')->willReturn(collect([]));\n app()->instance(FieldDataRepository::class, $fieldDataRepository);\n\n $config = $this->createMock(Configuration::class);\n $profilesRelation = $this->getMockBuilder(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class)\n ->disableOriginalConstructor()\n ->onlyMethods(['get'])\n ->addMethods(['where'])\n ->getMock();\n $profilesRelation->method('where')->willReturnSelf();\n $profilesRelation->method('get')->willReturn(collect([]));\n $config->method('profiles')->willReturn($profilesRelation);\n\n $serviceMock->config = $config;\n $serviceMock->profile = null;\n\n $actualStartTime = \\Carbon\\Carbon::now();\n\n $activity = $this->getMockBuilder(Activity::class)\n ->disableOriginalConstructor()\n ->onlyMethods(['update', 'hasProspect'])\n ->getMock();\n\n $activity->method('update')->willReturn(true);\n $activity->method('hasProspect')->willReturn(true);\n\n $activity->type = Activity::TYPE_CONFERENCE;\n $activity->provider = Activity::PROVIDER_TWILIO;\n $activity->lead_id = 1;\n $activity->user_id = 0;\n $activity->id_string = 'test-activity-id';\n $activity->user = $user;\n\n $activity->actual_start_time = $actualStartTime;\n $activity->uuid = 'c53d8320-f556-4cee-a2f8-5f232f454ca4';\n\n app()->bind(PlaybookResolver::class, function () use ($user) {\n $playbook = $this->createMock(Playbook::class);\n $playbookResolver = $this->createMock(PlaybookResolver::class);\n $playbookResolver->expects($this->once())\n ->method('resolvePlaybookByUser')\n ->with($user)\n ->willReturn($playbook);\n\n return $playbookResolver;\n });\n\n $data = $serviceMock->fetchAndAssociateRelatedActivity($activity);\n\n $this->assertInstanceOf(Activity::class, $data);\n $this->assertEquals(Activity::TYPE_CONFERENCE, $data->getType());\n $this->assertEquals($actualStartTime->getTimestamp(), $data->getActualStartTime()->getTimestamp());\n }\n\n public function testFetchAndAssociateRelatedActivitySkipsForTaskBasedPlaybook(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods(['fetchRelatedActivity', 'getPlaybook'])\n ->getMock();\n\n $user = $this->createMock(User::class);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n $playbook->method('getId')->willReturn(123);\n\n $serviceMock->expects($this->once())\n ->method('getPlaybook')\n ->with($user)\n ->willReturn($playbook);\n\n $serviceMock->expects($this->never())\n ->method('fetchRelatedActivity');\n\n $activity = $this->getMockBuilder(Activity::class)\n ->disableOriginalConstructor()\n ->onlyMethods(['hasProspect', 'getUuid'])\n ->getMock();\n\n $activity->method('hasProspect')->willReturn(true);\n $activity->method('getUuid')->willReturn('c53d8320-f556-4cee-a2f8-5f232f454ca4');\n $activity->type = Activity::TYPE_CONFERENCE;\n $activity->actual_start_time = \\Carbon\\Carbon::now();\n $activity->user = $user;\n\n $result = $serviceMock->fetchAndAssociateRelatedActivity($activity);\n\n $this->assertNull($result);\n }\n\n public function testFetchAndAssociateRelatedActivityReturnsNullForNonConference(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $serviceMock = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class)\n );\n\n $activity = $this->getMockBuilder(Activity::class)\n ->disableOriginalConstructor()\n ->getMock();\n\n $activity->type = Activity::TYPE_SOFTPHONE;\n\n $result = $serviceMock->fetchAndAssociateRelatedActivity($activity);\n\n $this->assertNull($result);\n }\n\n public function testFetchAndAssociateRelatedActivityReturnsNullWhenNoStartTime(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $serviceMock = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class)\n );\n\n\n $activity = $this->getMockBuilder(Activity::class)\n ->disableOriginalConstructor()\n ->getMock();\n\n $activity->type = Activity::TYPE_CONFERENCE;\n $activity->actual_start_time = null;\n $activity->scheduled_start_time = null;\n\n $result = $serviceMock->fetchAndAssociateRelatedActivity($activity);\n\n $this->assertNull($result);\n }\n\n public function testFetchAndAssociateRelatedActivityReturnsNullWhenNoProspect(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods(['getPlaybook'])\n ->getMock();\n\n $serviceMock->expects($this->never())\n ->method('getPlaybook');\n\n $activity = $this->getMockBuilder(Activity::class)\n ->disableOriginalConstructor()\n ->onlyMethods(['hasProspect', 'getUuid'])\n ->getMock();\n\n $activity->method('hasProspect')->willReturn(false);\n $activity->method('getUuid')->willReturn('c53d8320-f556-4cee-a2f8-5f232f454ca4');\n $activity->type = Activity::TYPE_CONFERENCE;\n $activity->actual_start_time = \\Carbon\\Carbon::now();\n\n $result = $serviceMock->fetchAndAssociateRelatedActivity($activity);\n\n $this->assertNull($result);\n }\n\n public function testMatchExactlyByEmail(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods([])\n ->getMock();\n\n $profile = new Profile();\n $profile->setAttribute('id', bin2hex(random_bytes(8)));\n $serviceMock->profile = $profile;\n\n $team = $this->createMock(Team::class);\n $serviceMock->team = $team;\n\n $data = $serviceMock->matchExactlyByEmail(bin2hex(random_bytes(8)) . 'test_email@testserver.com');\n\n $this->assertEquals(null, $data);\n }\n\n public function testMatchDomainFromEmail(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $queryIterator = $this->createMock(QueryIterator::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $config = $this->createMock(Configuration::class);\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->any())\n ->method('search')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods(['convertCrmData'])\n ->getMock();\n\n $profile = new Profile();\n $profile->account_fields = 'Field1, Field2, Field3';\n $serviceMock->profile = $profile;\n\n $serviceMock->expects($this->once())\n ->method('convertCrmData')\n ->willReturn(['test']);\n\n $this->app->bind(QueryBuilder::class, function () {\n $queryBuilder = $this->createMock(QueryBuilder::class);\n $queryBuilder->expects($this->once())\n ->method('buildMatchByDomainQuery')\n ->with('test_email@testserver.com')\n ->willReturn('FIND {test_email@testserver.com} IN ALL FIELDS RETURNING Account(Id)');\n\n return $queryBuilder;\n });\n\n $team = $this->createMock(Team::class);\n\n $serviceMock->team = $team;\n $serviceMock->config = $config;\n\n $data = $serviceMock->matchByDomain('test_email@testserver.com');\n\n $this->assertEquals(['test'], $data);\n }\n\n public function testBuildTaskSearchFields(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class)\n );\n\n $fields = $service->buildTaskSearchFields();\n\n $expectedFields = ['Id', 'WhoId', 'WhatId', 'AccountId'];\n\n $this->assertEquals($expectedFields, $fields);\n }\n\n public function testMapCrmObjects(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class),\n );\n\n $sampleTask = [\n 'WhoId' => '003sampleWhoId',\n 'AccountId' => 'sampleAccountId',\n 'WhatId' => 'sampleWhatId',\n ];\n\n $activityData = $service->mapCrmObjects($sampleTask);\n\n $expectedActivityData = [\n 'contact' => '003sampleWhoId',\n 'account' => 'sampleAccountId',\n 'opportunity' => 'sampleWhatId',\n ];\n\n $this->assertEquals($expectedActivityData, $activityData);\n }\n\n public function testGetInstalledAppVersion(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n $queryIterator = $this->createMock(QueryIterator::class);\n $queryIterator->expects($this->any())\n ->method('current')->willReturn([\n 'SubscriberPackageVersion' => [\n 'MajorVersion' => '1',\n 'MinorVersion' => '0',\n 'PatchVersion' => '1',\n 'BuildNumber' => '0',\n ],\n ]);\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->any())\n ->method('metadata')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods(array_diff(get_class_methods(Service::class), ['getInstalledAppVersion']))\n ->getMock();\n\n $version = $serviceMock->getInstalledAppVersion();\n\n $this->assertEquals('1010', $version);\n }\n\n public function testSyncProfiles(): void\n {\n $userToSearch = null;\n\n $team = $this->createMock(Team::class);\n $config = $this->createMockedConfiguration();\n $config->expects($this->once())\n ->method('getId')\n ->willReturn(1);\n\n $salesforceUser = [\n 'Email' => 'test@example.com',\n 'UserPreferencesLightningExperiencePreferred' => true,\n 'CallCenterId' => '123',\n 'Id' => '456',\n 'ProfileId' => '789',\n ];\n\n app()->bind(QueryBuilder::class, function () use ($userToSearch) {\n $queryBuilder = $this->createMock(QueryBuilder::class);\n $queryBuilder->expects($this->once())\n ->method('buildGetUsersQuery')\n ->with($userToSearch)\n ->willReturn('SELECT * FROM Users');\n\n return $queryBuilder;\n });\n\n $queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults([$salesforceUser], 1, true, null));\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->any())\n ->method('query')\n ->with('SELECT * FROM Users')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n\n $user = $this->createMock(User::class);\n $user->expects($this->once())\n ->method('getId')\n ->willReturn(123);\n\n $this->mockTeamRepository($team, $salesforceUser, $user);\n\n $profileRepository = $this->createMock(ProfileRepository::class);\n $profileRepository->expects($this->once())\n ->method('updateOrCreateProfile')\n ->with(\n $user,\n [\n 'crm_configuration_id' => 1,\n 'crm_provider_id' => '456',\n ],\n [\n 'user_id' => 123,\n 'edition' => Profile::EDITION_LIGHTNING,\n 'has_external_cti' => true,\n 'crm_profile_id' => '789',\n ]\n )\n ->willReturn(new Profile());\n\n $this->app->instance(ProfileRepository::class, $profileRepository);\n\n $serviceMock = $this->getServiceMock();\n $serviceMock->team = $team;\n $serviceMock->config = $config;\n $result = $serviceMock->syncProfiles($userToSearch);\n\n $this->assertNull($result);\n }\n\n public function testSyncProfilesEmailIsNull(): void\n {\n $userToSearch = $this->createMock(User::class);\n\n $salesforceUser = [\n 'Email' => null,\n ];\n\n app()->bind(QueryBuilder::class, function () {\n $queryBuilder = $this->createMock(QueryBuilder::class);\n $queryBuilder->expects($this->once())\n ->method('buildGetUsersQuery')\n ->with(null)\n ->willReturn('SELECT * FROM Users');\n\n return $queryBuilder;\n });\n\n $queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults([$salesforceUser], 1, true, null));\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->exactly(2))\n ->method('query')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n\n $team = $this->createMock(Team::class);\n $user = $this->createMock(User::class);\n $this->mockTeamRepository($team, $salesforceUser, $user, false);\n\n $config = $this->createMock(Configuration::class);\n\n $serviceMock = $this->getServiceMock();\n $serviceMock->team = $team;\n $serviceMock->config = $config;\n\n $profile = $serviceMock->syncProfiles(null);\n\n $this->assertNull($profile);\n }\n\n public function testSyncProfilesUserToSearchMatchesCurrentUser(): void\n {\n $userToSearch = $this->createMock(User::class);\n $userToSearch->expects($this->once())\n ->method('getId')\n ->willReturn(123);\n\n $team = $this->createMock(Team::class);\n $config = $this->createMock(Configuration::class);\n $config->expects($this->once())\n ->method('getId')\n ->willReturn(1);\n\n $salesforceUser = [\n 'Email' => 'test@example.com',\n 'UserPreferencesLightningExperiencePreferred' => true,\n 'CallCenterId' => '123',\n 'Id' => '456',\n 'ProfileId' => '789',\n ];\n\n app()->bind(QueryBuilder::class, function () use ($userToSearch) {\n $queryBuilder = $this->createMock(QueryBuilder::class);\n $queryBuilder->expects($this->once())\n ->method('buildGetUsersQuery')\n ->with($userToSearch)\n ->willReturn('SELECT * FROM Users');\n\n return $queryBuilder;\n });\n\n $queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults([$salesforceUser], 1, true, null));\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->any())\n ->method('query')\n ->with('SELECT * FROM Users')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n\n $user = $this->createMock(User::class);\n $user->expects($this->exactly(2))\n ->method('getId')\n ->willReturn(123);\n\n $this->mockTeamRepository($team, $salesforceUser, $user);\n\n $profileRepository = $this->createMock(ProfileRepository::class);\n $profileRepository->expects($this->once())\n ->method('updateOrCreateProfile')\n ->with(\n $user,\n [\n 'crm_configuration_id' => 1,\n 'crm_provider_id' => '456',\n ],\n [\n 'user_id' => 123,\n 'edition' => Profile::EDITION_LIGHTNING,\n 'has_external_cti' => true,\n 'crm_profile_id' => '789',\n ]\n )\n ->willReturn(new Profile());\n\n $this->app->instance(ProfileRepository::class, $profileRepository);\n\n $serviceMock = $this->getServiceMock();\n $serviceMock->team = $team;\n $serviceMock->config = $config;\n $profile = $serviceMock->syncProfiles($userToSearch);\n\n $this->assertInstanceOf(Profile::class, $profile);\n }\n\n public function testSyncProfilesWithCustomValidation(): void\n {\n $userToSearch = null;\n\n $team = $this->createMock(Team::class);\n $config = $this->createMockedConfiguration();\n $config->expects($this->atLeastOnce()) // Changed from once() to atLeastOnce()\n ->method('getId')\n ->willReturn(1);\n\n $salesforceUser = [\n 'Email' => 'test@example.com',\n 'UserPreferencesLightningExperiencePreferred' => true,\n 'CallCenterId' => '123',\n 'Id' => '456',\n 'ProfileId' => '789',\n 'CustomField' => 'CustomValue',\n ];\n\n $customRules = [\n ['field' => 'CustomField', 'value' => 'CustomValue'],\n ];\n\n $this->mockQueryBuilderAndHandler($userToSearch, [$salesforceUser]);\n\n $user = $this->createMock(User::class);\n $user->expects($this->once())\n ->method('getId')\n ->willReturn(123);\n\n $this->mockTeamRepository($team, $salesforceUser, $user, true, $customRules);\n\n $this->mockProfileRepository($user);\n\n $serviceMock = $this->getServiceMock();\n $serviceMock->team = $team;\n $serviceMock->config = $config;\n $profile = $serviceMock->syncProfiles($userToSearch);\n\n $this->assertNull($profile);\n }\n\n public function testSyncProfilesWithCustomValidationFailing(): void\n {\n $userToSearch = null;\n\n $team = $this->createMock(Team::class);\n $config = $this->createMockedConfiguration();\n\n $salesforceUser = [\n 'Email' => 'test@example.com',\n 'UserPreferencesLightningExperiencePreferred' => true,\n 'CallCenterId' => '123',\n 'Id' => '456',\n 'ProfileId' => '789',\n 'CustomField' => 'WrongValue',\n ];\n\n $customRules = [\n ['field' => 'CustomField', 'value' => 'CustomValue'],\n ];\n\n $this->mockQueryBuilderAndHandler($userToSearch, [$salesforceUser]);\n\n $teamRepository = $this->getMockForAbstractClass(TeamRepository::class, [], '', false, true, true, ['findActiveTeamMemberByEmail', 'getTeamSetting']);\n\n $teamSettings = $this->createMock(TeamSettings::class);\n $teamSettings->method('getValueType')\n ->willReturn('array');\n\n $teamSettings->method('getValue')\n ->willReturn(json_encode($customRules));\n\n $teamRepository->expects($this->once())\n ->method('getTeamSetting')\n ->with($team, 'custom_profile_validation')\n ->willReturn($teamSettings);\n\n app()->bind(TeamRepository::class, function () use ($teamRepository) {\n return $teamRepository;\n });\n\n $profileRepository = $this->createMock(ProfileRepository::class);\n $profileRepository->expects($this->never())\n ->method('updateOrCreateProfile');\n\n $this->app->instance(ProfileRepository::class, $profileRepository);\n\n $serviceMock = $this->getServiceMock();\n $serviceMock->team = $team;\n $serviceMock->config = $config;\n $result = $serviceMock->syncProfiles($userToSearch);\n\n $this->assertNull($result);\n }\n\n public function testGetContactRolesFromCrm(): void\n {\n $contactRoles = [\n [\n 'Id' => '1',\n 'ContactId' => 'Contact1',\n 'OpportunityId' => 'Opportunity1',\n 'Opportunity' => ['OwnerId' => 'Owner1'],\n 'IsPrimary' => true,\n 'Role' => 'Decision Maker',\n ],\n ];\n\n $expectedResponse = [\n [\n 'id' => '1',\n 'contactId' => 'Contact1',\n 'opportunityId' => 'Opportunity1',\n 'ownerId' => 'Owner1',\n 'isPrimary' => true,\n 'role' => 'Decision Maker',\n ],\n ];\n\n $this->bindQueryIterator($contactRoles);\n\n $serviceMock = $this->getServiceMock();\n\n $data = $serviceMock->getContactRolesFromCrm(now()->subDay());\n\n $this->assertEquals($expectedResponse, $data);\n }\n\n public function testGetContactRolesFromCrmNoResult(): void\n {\n $this->bindQueryIterator([]);\n\n $serviceMock = $this->getServiceMock();\n\n $data = $serviceMock->getContactRolesFromCrm(now()->subDay());\n\n $this->assertEquals([], $data);\n }\n\n public function testSyncContactRoles(): void\n {\n $contactRoles = [\n [\n 'id' => '1',\n 'contactId' => 'Contact1',\n 'opportunityId' => 'Opportunity1',\n 'ownerId' => 'Owner1',\n 'isPrimary' => true,\n 'role' => 'Decision Maker',\n ],\n ];\n\n app()->bind(ContactRoleRepository::class, function () {\n $contactRoleRepository = $this->createMock(ContactRoleRepository::class);\n $contactRoleRepository->expects($this->once())\n ->method('saveContactRoles');\n\n return $contactRoleRepository;\n });\n\n $serviceMock = $this->getServiceMock([\n 'getContactRolesFromCrm',\n 'syncRemotelyDeletedContactRoles',\n 'syncContact',\n 'syncOpportunity',\n ]);\n\n $config = $this->createMock(Configuration::class);\n $hasMany = $this->createMock(HasManyExtended::class);\n $hasMany->expects($this->exactly(2))\n ->method('where')\n ->willReturn($hasMany);\n\n $hasMany->expects($this->exactly(2))\n ->method('first')\n ->willReturn(\n $this->createMock(Contact::class),\n $this->createMock(Opportunity::class)\n );\n\n $config->expects($this->once())\n ->method('contacts')\n ->willReturn($hasMany);\n $config->expects($this->once())\n ->method('opportunities')\n ->willReturn($hasMany);\n\n $serviceMock->config = $config;\n\n $serviceMock->expects($this->once())\n ->method('getContactRolesFromCrm')\n ->willReturn($contactRoles);\n\n $serviceMock->expects($this->once())\n ->method('syncRemotelyDeletedContactRoles');\n\n $data = $serviceMock->syncContactRoles(now()->subDay());\n\n $this->assertEquals(1, $data);\n }\n\n public function testSyncRemotelyDeletedContactRoles(): void\n {\n $contactRoles = [\n [\n 'id' => '1',\n 'crm_provider_id' => '1',\n ],\n ];\n\n app()->bind(QueryHandler::class, function () use ($contactRoles) {\n $queryResults = new QueryResults($contactRoles, 1, true, null);\n\n $handler = $this->createMock(QueryHandler::class);\n $handler->method('queryDeleted')\n ->willReturn($queryResults);\n\n return $handler;\n });\n\n app()->bind(ContactRoleRepository::class, function () {\n $contactRoleRepository = $this->createMock(ContactRoleRepository::class);\n $contactRoleRepository->expects($this->once())\n ->method('deleteContactRoles');\n\n return $contactRoleRepository;\n });\n\n $serviceMock = $this->getServiceMock();\n $serviceMock->team = $this->createMock(Team::class);\n\n $data = $this->invokePrivateMethod('syncRemotelyDeletedContactRoles', $serviceMock, []);\n\n $this->assertTrue($data);\n }\n\n private function bindQueryIterator(array $queryResult): void\n {\n /** @var Client $client */\n $client = $this->createMock(Client::class);\n $queryIterator = new QueryIterator(\n $client,\n new QueryResults($queryResult, 1, true, null)\n );\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->any())\n ->method('query')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n }\n\n public static function getOpportunitySortOrderDataProvider(): array\n {\n return [\n 'all open recently updated' => [Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED, ['LastModifiedDate DESC', true]],\n 'all open recently created' => [Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED, ['CreatedDate DESC', true]],\n 'all open oldest created' => [Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED, ['CreatedDate ASC', true]],\n 'all recently updated' => [Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED, ['LastModifiedDate DESC', false]],\n 'default' => ['unknown', ['LastModifiedDate DESC', true]],\n ];\n }\n\n private function createMockedConfiguration(): Configuration\n {\n $config = $this->createMock(Configuration::class);\n $profilesRelation = $this->getMockBuilder(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class)\n ->disableOriginalConstructor()\n ->onlyMethods(['get'])\n ->addMethods(['where', 'first'])\n ->getMock();\n $profilesRelation->method('where')->willReturnSelf();\n $profilesRelation->method('get')->willReturn(collect([]));\n $profilesRelation->method('first')->willReturn(null);\n $config->method('profiles')->willReturn($profilesRelation);\n\n return $config;\n }\n\n private function getServiceMock(array $onlyMethods = []): MockObject&Service\n {\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $this->createMock(Client::class),\n $this->createMock(PayloadBuilder::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(CountriesMap::class),\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods($onlyMethods)\n ->getMock();\n\n $serviceMock->profile = $this->createMock(Profile::class);\n\n return $serviceMock;\n }\n\n private function mockQueryBuilderAndHandler($userToSearch, $salesforceUsers): void\n {\n app()->bind(QueryBuilder::class, function () use ($userToSearch) {\n $queryBuilder = $this->createMock(QueryBuilder::class);\n $queryBuilder->expects($this->once())\n ->method('buildGetUsersQuery')\n ->with($userToSearch)\n ->willReturn('SELECT * FROM Users');\n\n return $queryBuilder;\n });\n\n $queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults($salesforceUsers, count($salesforceUsers), true, null));\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->any())\n ->method('query')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n }\n\n private function mockTeamRepository(\n Team $team,\n array $salesforceUser,\n ?User $user = null,\n bool $userSearch = true,\n array $customRules = []\n ): void {\n $teamRepository = $this->getMockForAbstractClass(TeamRepository::class, [], '', false, true, true, ['findActiveTeamMemberByEmail', 'getTeamSetting']);\n\n if ($userSearch) {\n $teamRepository->expects($this->once())\n ->method('findActiveTeamMemberByEmail')\n ->with($team, $salesforceUser['Email'])\n ->willReturn($user);\n }\n\n $teamSettings = $this->createMock(TeamSettings::class);\n $teamSettings->method('getValueType')\n ->willReturn('array');\n\n $teamSettings->method('getValue')\n ->willReturn(json_encode($customRules));\n\n $teamRepository->expects($this->once())\n ->method('getTeamSetting')\n ->with($team, 'custom_profile_validation')\n ->willReturn($teamSettings);\n\n app()->bind(TeamRepository::class, function () use ($teamRepository) {\n return $teamRepository;\n });\n }\n\n private function mockProfileRepository(User $user): void\n {\n $profileRepository = $this->createMock(ProfileRepository::class);\n $profileRepository->expects($this->once())\n ->method('updateOrCreateProfile')\n ->with(\n $user,\n [\n 'crm_configuration_id' => 1,\n 'crm_provider_id' => '456',\n ],\n [\n 'user_id' => 123,\n 'edition' => Profile::EDITION_LIGHTNING,\n 'has_external_cti' => true,\n 'crm_profile_id' => '789',\n ]\n )\n ->willReturn(new Profile());\n\n $this->app->instance(ProfileRepository::class, $profileRepository);\n }\n\n public function testBuildEnhancedNoteDecodesHtmlEntities(): void\n {\n $service = $this->getServiceMock(['createRecord']);\n\n $profile = new Profile();\n $profile->setAttribute('crm_provider_id', 'owner-123');\n $service->profile = $profile;\n\n $service->expects($this->exactly(2))\n ->method('createRecord')\n ->willReturnOnConsecutiveCalls('note-id-123', 'link-id-456');\n\n $bodyWithEntities = 'Welch's current challenges and Facebook's Club';\n\n $result = $this->invokePrivateMethod('buildEnhancedNote', $service, [\n 'Test Title',\n $bodyWithEntities,\n 'object-id-789',\n ]);\n\n $this->assertEquals('note-id-123', $result);\n }\n\n public function testBuildEnhancedNoteSanitizesWithoutQuotes(): void\n {\n $service = $this->getServiceMock(['createRecord']);\n\n $profile = new Profile();\n $profile->setAttribute('crm_provider_id', 'owner-456');\n $service->profile = $profile;\n\n $service->expects($this->exactly(2))\n ->method('createRecord')\n ->willReturnCallback(function ($type, $data) {\n if ($type === 'ContentNote') {\n $decoded = base64_decode($data['Content']);\n $this->assertStringContainsString(\"Welch's\", $decoded);\n $this->assertStringNotContainsString(''', $decoded);\n $this->assertStringNotContainsString('&#039;', $decoded);\n $this->assertStringContainsString('<script>', $decoded);\n\n return 'note-id-456';\n }\n\n return 'link-id-789';\n });\n\n $bodyWithMixedContent = \"Welch's and <script>alert('xss')</script>\";\n\n $result = $this->invokePrivateMethod('buildEnhancedNote', $service, [\n 'Test Title',\n $bodyWithMixedContent,\n 'object-id-123',\n ]);\n\n $this->assertEquals('note-id-456', $result);\n }\n\n public function testBuildEnhancedNoteConvertsLineBreaks(): void\n {\n $service = $this->getServiceMock(['createRecord']);\n\n $profile = new Profile();\n $profile->setAttribute('crm_provider_id', 'owner-789');\n $service->profile = $profile;\n\n $service->expects($this->exactly(2))\n ->method('createRecord')\n ->willReturnCallback(function ($type, $data) {\n if ($type === 'ContentNote') {\n $decoded = base64_decode($data['Content']);\n $this->assertStringContainsString('<br>', $decoded);\n $this->assertStringNotContainsString('<br />', $decoded);\n\n return 'note-id-789';\n }\n\n return 'link-id-012';\n });\n\n $bodyWithLineBreaks = \"Line 1\\nLine 2\\nLine 3\";\n\n $result = $this->invokePrivateMethod('buildEnhancedNote', $service, [\n 'Test Title',\n $bodyWithLineBreaks,\n 'object-id-456',\n ]);\n\n $this->assertEquals('note-id-789', $result);\n }\n\n public function testBuildEnhancedNoteHandlesComplexScenario(): void\n {\n $service = $this->getServiceMock(['createRecord']);\n\n $profile = new Profile();\n $profile->setAttribute('crm_provider_id', 'owner-complex');\n $service->profile = $profile;\n\n $service->expects($this->exactly(2))\n ->method('createRecord')\n ->willReturnCallback(function ($type, $data) {\n if ($type === 'ContentNote') {\n $decoded = base64_decode($data['Content']);\n\n $this->assertStringContainsString(\"Welch's\", $decoded);\n $this->assertStringContainsString(\"Facebook's Club\", $decoded);\n $this->assertStringContainsString(\"Arctics'\", $decoded);\n $this->assertStringNotContainsString(''', $decoded);\n $this->assertStringNotContainsString('&#039;', $decoded);\n $this->assertStringContainsString('<br>', $decoded);\n $this->assertStringContainsString('<', $decoded);\n $this->assertStringContainsString('>', $decoded);\n\n return 'note-complex';\n }\n\n return 'link-complex';\n });\n\n $complexBody = \"Summary:\\n---------\\nThe call focused on understanding Welch's current challenges and exploring how Arctics' Revenue Growth Management solutions could support their strategic goals.\\n\\n• John SMith discussed his role as a category advisor for Google and Facebook's Club, emphasizing the importance of market research and advising on product assortment.\\n• Madona introduced Arctics' Virtual Shoppers AI, which simulates consumer <behavior> to optimize pricing and promotional strategies.\";\n\n $result = $this->invokePrivateMethod('buildEnhancedNote', $service, [\n 'Jiminny Transcription Summary',\n $complexBody,\n 'task-id-001',\n ]);\n\n $this->assertEquals('note-complex', $result);\n }\n\n public function testSyncRemotelyDeletedObjectsWithErrorHandlingSuccess(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['syncRemotelyDeletedObjects'])\n ->getMock();\n\n // Mock team\n $team = $this->createMock(Team::class);\n $team->method('getUuid')->willReturn('team-uuid-123');\n $serviceMock->team = $team;\n\n // Expect syncRemotelyDeletedObjects to be called once and succeed\n $serviceMock->expects($this->once())\n ->method('syncRemotelyDeletedObjects')\n ->with(\\Jiminny\\Enums\\CrmObject::ACCOUNT);\n\n // Call the protected method using reflection\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');\n $method->setAccessible(true);\n\n // Should not throw any exceptions\n $method->invoke($serviceMock, \\Jiminny\\Enums\\CrmObject::ACCOUNT);\n\n $this->assertTrue(true); // Test completed successfully\n }\n\n public function testSyncRemotelyDeletedObjectsWithErrorHandlingFailure(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['syncRemotelyDeletedObjects'])\n ->getMock();\n\n // Mock team\n $team = $this->createMock(Team::class);\n $team->method('getUuid')->willReturn('team-uuid-456');\n $serviceMock->team = $team;\n\n // Mock logger to verify warning is logged\n $logger = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n\n // Use reflection to set the protected logger property\n $reflection = new \\ReflectionClass($serviceMock);\n $loggerProperty = $reflection->getProperty('logger');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($serviceMock, $logger);\n\n $exception = new \\Exception('Sync failed due to API error');\n\n // Expect syncRemotelyDeletedObjects to throw an exception\n $serviceMock->expects($this->once())\n ->method('syncRemotelyDeletedObjects')\n ->with(\\Jiminny\\Enums\\CrmObject::CONTACT)\n ->willThrowException($exception);\n\n // Expect warning to be logged with correct message and parameters\n $logger->expects($this->once())\n ->method('warning')\n ->with(\n '[Salesforce] Remotely deleted objects sync failed',\n [\n 'objectType' => 'contact',\n 'teamId' => 'team-uuid-456',\n 'reason' => 'Sync failed due to API error',\n ]\n );\n\n // Call the protected method using reflection\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');\n $method->setAccessible(true);\n\n // Should not re-throw the exception, just log it\n $method->invoke($serviceMock, \\Jiminny\\Enums\\CrmObject::CONTACT);\n\n $this->assertTrue(true); // Test completed successfully\n }\n\n public function testSyncRemotelyDeletedObjectsWithErrorHandlingWithLogParams(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['syncRemotelyDeletedObjects'])\n ->getMock();\n\n // Mock team\n $team = $this->createMock(Team::class);\n $team->method('getUuid')->willReturn('team-uuid-789');\n $serviceMock->team = $team;\n\n // Mock logger to verify warning is logged\n $logger = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n\n // Use reflection to set the protected logger property\n $loggerReflection = new \\ReflectionClass($serviceMock);\n $loggerProperty = $loggerReflection->getProperty('logger');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($serviceMock, $logger);\n\n $exception = new \\Exception('Network timeout');\n\n // Expect syncRemotelyDeletedObjects to throw an exception\n $serviceMock->expects($this->once())\n ->method('syncRemotelyDeletedObjects')\n ->with(\\Jiminny\\Enums\\CrmObject::OPPORTUNITY)\n ->willThrowException($exception);\n\n // Additional log parameters\n $logParams = [\n 'syncType' => 'full',\n 'batchSize' => 100,\n ];\n\n // Expect warning to be logged with merged parameters\n $logger->expects($this->once())\n ->method('warning')\n ->with(\n '[Salesforce] Remotely deleted objects sync failed',\n [\n 'objectType' => 'opportunity',\n 'teamId' => 'team-uuid-789',\n 'reason' => 'Network timeout',\n 'syncType' => 'full',\n 'batchSize' => 100,\n ]\n );\n\n // Call the protected method using reflection\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');\n $method->setAccessible(true);\n\n // Should not re-throw the exception, just log it\n $method->invoke($serviceMock, \\Jiminny\\Enums\\CrmObject::OPPORTUNITY, $logParams);\n\n $this->assertTrue(true); // Test completed successfully\n }\n\n /**\n * @dataProvider crmObjectProvider\n */\n public function testSyncRemotelyDeletedObjectsWithErrorHandlingDifferentCrmObjects(\\Jiminny\\Enums\\CrmObject $crmObject): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['syncRemotelyDeletedObjects'])\n ->getMock();\n\n // Mock team\n $team = $this->createMock(Team::class);\n $team->method('getUuid')->willReturn('team-uuid-test');\n $serviceMock->team = $team;\n\n // Mock logger to verify warning is logged\n $logger = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n\n // Use reflection to set the protected logger property\n $loggerReflectionClass = new \\ReflectionClass($serviceMock);\n $loggerProperty = $loggerReflectionClass->getProperty('logger');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($serviceMock, $logger);\n\n $exception = new \\Exception('Test error');\n\n // Expect syncRemotelyDeletedObjects to throw an exception\n $serviceMock->expects($this->once())\n ->method('syncRemotelyDeletedObjects')\n ->with($crmObject)\n ->willThrowException($exception);\n\n // Expect warning to be logged with correct entity type\n $logger->expects($this->once())\n ->method('warning')\n ->with(\n '[Salesforce] Remotely deleted objects sync failed',\n [\n 'objectType' => $crmObject->value,\n 'teamId' => 'team-uuid-test',\n 'reason' => 'Test error',\n ]\n );\n\n // Call the protected method using reflection\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');\n $method->setAccessible(true);\n\n $method->invoke($serviceMock, $crmObject);\n\n $this->assertTrue(true); // Test completed successfully\n }\n\n public function testHandleObjectDeletionWithDeletedEntity(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['deleteCrmObject'])\n ->getMock();\n\n $entity = $this->createMock(\\Jiminny\\Models\\Account::class);\n $crmData = ['IsDeleted' => true];\n\n $serviceMock->expects($this->once())\n ->method('deleteCrmObject')\n ->with($entity);\n\n // Use reflection to call the protected method\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('handleObjectDeletion');\n $method->setAccessible(true);\n\n $method->invoke($serviceMock, $entity, $crmData);\n }\n\n public function testHandleObjectDeletionWithNonDeletedEntity(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['deleteCrmObject'])\n ->getMock();\n\n $entity = $this->createMock(\\Jiminny\\Models\\Contact::class);\n $crmData = ['IsDeleted' => false];\n\n $serviceMock->expects($this->never())\n ->method('deleteCrmObject');\n\n // Use reflection to call the protected method\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('handleObjectDeletion');\n $method->setAccessible(true);\n\n $method->invoke($serviceMock, $entity, $crmData);\n }\n\n public function testDeleteCrmObjectWithValidEntity(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['dispatchDeleteCrmObjectJob'])\n ->getMock();\n\n $entity = $this->createMock(\\Jiminny\\Models\\Lead::class);\n $entity->expects($this->once())->method('delete');\n\n $serviceMock->expects($this->once())\n ->method('dispatchDeleteCrmObjectJob')\n ->with($entity);\n\n // Use reflection to call the protected method\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('deleteCrmObject');\n $method->setAccessible(true);\n\n $method->invoke($serviceMock, $entity);\n }\n\n public function testDeleteCrmObjectWithNullEntity(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['dispatchDeleteCrmObjectJob'])\n ->getMock();\n\n $serviceMock->expects($this->never())\n ->method('dispatchDeleteCrmObjectJob');\n\n // Use reflection to call the protected method\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('deleteCrmObject');\n $method->setAccessible(true);\n\n $method->invoke($serviceMock, null);\n }\n\n public function testDispatchDeleteCrmObjectJobWithNullEntity(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $prospectPhotoPathService,\n );\n\n // Use reflection to call the protected method\n $reflection = new \\ReflectionClass($service);\n $method = $reflection->getMethod('dispatchDeleteCrmObjectJob');\n $method->setAccessible(true);\n\n // Should return early without dispatching - no exception expected\n $method->invoke($service, null);\n\n $this->assertTrue(true); // Test completed successfully\n }\n\n public function testDispatchDeleteCrmObjectJobWithUnsupportedEntity(): void\n {\n $this->expectException(\\TypeError::class);\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $prospectPhotoPathService,\n );\n\n $unsupportedEntity = $this->createMock(\\stdClass::class);\n\n // Use reflection to call the protected method\n $reflection = new \\ReflectionClass($service);\n $method = $reflection->getMethod('dispatchDeleteCrmObjectJob');\n\n // This will throw TypeError due to union type constraint\n $method->invoke($service, $unsupportedEntity);\n }\n\n public function testHandleEntityDeletionByProviderIdMethodExists(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $prospectPhotoPathService,\n );\n\n // Test that the method exists and is accessible via reflection\n $reflection = new \\ReflectionClass($service);\n $method = $reflection->getMethod('handleEntityDeletionByProviderId');\n $method->setAccessible(true);\n\n // Verify method exists and has correct parameters\n $this->assertTrue($method->isProtected());\n $this->assertEquals(2, $method->getNumberOfParameters());\n\n $parameters = $method->getParameters();\n $this->assertEquals('targetEntity', $parameters[0]->getName());\n $this->assertEquals('crmData', $parameters[1]->getName());\n }\n\n public function testSyncRemotelyDeletedObjectsWithNoResults(): void\n {\n // Create a real service instance to avoid mock property issues\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $prospectPhotoPathService,\n );\n\n // Mock queryHandler to throw NoResultsException\n $queryHandler = $this->createMock(QueryHandler::class);\n $queryHandler->expects($this->once())\n ->method('queryDeleted')\n ->with('Opportunity')\n ->willThrowException(new NoResultsException('No results'));\n\n // Set the queryHandler using reflection on the real service\n $reflection = new \\ReflectionClass($service);\n $queryHandlerProperty = $reflection->getProperty('queryHandler');\n $queryHandlerProperty->setAccessible(true);\n $queryHandlerProperty->setValue($service, $queryHandler);\n\n $result = self::invokePrivateMethod('syncRemotelyDeletedObjects', $service, [CrmObject::OPPORTUNITY]);\n\n $this->assertFalse($result);\n }\n\n public function testSyncRemotelyDeletedObjectsWithEmptyResults(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $prospectPhotoPathService,\n );\n\n // Mock queryHandler to return empty results\n $queryResult = $this->createMock(QueryResults::class);\n $queryResult->method('getResults')->willReturn([]);\n\n $queryHandler = $this->createMock(QueryHandler::class);\n $queryHandler->expects($this->once())\n ->method('queryDeleted')\n ->with('Opportunity')\n ->willReturn($queryResult);\n\n // Set the queryHandler using reflection on the real service\n $reflection = new \\ReflectionClass($service);\n $queryHandlerProperty = $reflection->getProperty('queryHandler');\n $queryHandlerProperty->setAccessible(true);\n $queryHandlerProperty->setValue($service, $queryHandler);\n\n $result = self::invokePrivateMethod('syncRemotelyDeletedObjects', $service, [CrmObject::OPPORTUNITY]);\n\n $this->assertFalse($result);\n }\n\n public function testSyncRemotelyDeletedObjectsWithUnsupportedCrmObject(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $prospectPhotoPathService,\n );\n\n // Mock queryHandler to return some deleted objects so we reach the match statement\n $deletedObjects = [\n ['id' => 'task1'],\n ['id' => 'task2'],\n ];\n $queryResult = $this->createMock(QueryResults::class);\n $queryResult->method('getResults')->willReturn($deletedObjects);\n\n $queryHandler = $this->createMock(QueryHandler::class);\n $queryHandler->expects($this->once())\n ->method('queryDeleted')\n ->with('Task') // ucfirst('task') = 'Task'\n ->willReturn($queryResult);\n\n self::setPrivateProperty($service, 'queryHandler', $queryHandler);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Unsupported CrmObject: task');\n\n self::invokePrivateMethod('syncRemotelyDeletedObjects', $service, [CrmObject::TASK]);\n }\n\n public static function crmObjectProvider(): array\n {\n return [\n 'Account' => [CrmObject::ACCOUNT],\n 'Contact' => [CrmObject::CONTACT],\n 'Lead' => [CrmObject::LEAD],\n 'Opportunity' => [CrmObject::OPPORTUNITY],\n ];\n }\n\n public function testVerifyTaskExistsReturnsTrue(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:task-123', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-123');\n $activity->method('getId')->willReturn(456);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Task', 'task-123', ['Id', 'IsDeleted'])\n ->willReturn(['Id' => 'task-123', 'IsDeleted' => false]);\n\n $result = $service->verifyTaskExists($activity);\n\n $this->assertTrue($result);\n }\n\n public function testVerifyTaskExistsReturnsTrueForEvent(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:event-123', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('event-123');\n $activity->method('getId')->willReturn(456);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_EVENT);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Event', 'event-123', ['Id', 'IsDeleted'])\n ->willReturn(['Id' => 'event-123', 'IsDeleted' => false]);\n\n $result = $service->verifyTaskExists($activity);\n\n $this->assertTrue($result);\n }\n\n public function testVerifyTaskExistsReturnsFalseWhenDeleted(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:task-456', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-456');\n $activity->method('getId')->willReturn(789);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Task', 'task-456', ['Id', 'IsDeleted'])\n ->willReturn(['Id' => 'task-456', 'IsDeleted' => true]);\n\n $result = $service->verifyTaskExists($activity);\n\n $this->assertFalse($result);\n }\n\n public function testVerifyTaskExistsReturnsFalseWhenNotFound(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:task-999', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-999');\n $activity->method('getId')->willReturn(999);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Task', 'task-999', ['Id', 'IsDeleted'])\n ->willThrowException(new \\Jiminny\\Exceptions\\HttpNotFoundException('Task not found'));\n\n $result = $service->verifyTaskExists($activity);\n\n $this->assertFalse($result);\n }\n\n public function testVerifyTaskExistsReturnsFalseWhenNoPlaybook(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:task-no-playbook', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-no-playbook');\n $activity->method('getId')->willReturn(111);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn(null);\n\n $result = $service->verifyTaskExists($activity);\n\n $this->assertFalse($result);\n }\n\n public function testVerifyTaskExistsThrowsExceptionForTransientErrors(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:task-error', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-error');\n $activity->method('getId')->willReturn(888);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Task', 'task-error', ['Id', 'IsDeleted'])\n ->willThrowException(new \\RuntimeException('Network timeout'));\n\n $this->expectException(\\RuntimeException::class);\n $this->expectExceptionMessage('Network timeout');\n\n $service->verifyTaskExists($activity);\n }\n\n public function testVerifyTaskExistsCachesResults(): void\n {\n $cachedValue = null;\n Cache::shouldReceive('remember')\n ->twice()\n ->with('crm_task_exists:123:task-cached', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(function ($key, $ttl, $callback) use (&$cachedValue) {\n if ($cachedValue === null) {\n $cachedValue = $callback();\n }\n\n return $cachedValue;\n });\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-cached');\n $activity->method('getId')->willReturn(555);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Task', 'task-cached', ['Id', 'IsDeleted'])\n ->willReturn(['Id' => 'task-cached', 'IsDeleted' => false]);\n\n $result1 = $service->verifyTaskExists($activity);\n $result2 = $service->verifyTaskExists($activity);\n\n $this->assertTrue($result1);\n $this->assertTrue($result2);\n }\n\n public function testVerifyTaskExistsReturnsFalseForHttpBadRequestException(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:task-400', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-400');\n $activity->method('getId')->willReturn(400);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Task', 'task-400', ['Id', 'IsDeleted'])\n ->willThrowException(new \\Jiminny\\Exceptions\\HttpBadRequestException('Bad request'));\n\n $result = $service->verifyTaskExists($activity);\n\n $this->assertFalse($result);\n }\n\n public function testImportOpportunitySkipsWhenNoProfileAndNoAccount(): void\n {\n $crmData = [\n 'Id' => 'SF-NO-USER-1',\n 'Name' => 'Test Opportunity',\n 'OwnerId' => 'owner-no-profile',\n // No AccountId\n ];\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(\\Illuminate\\Events\\Dispatcher::class); // ← ADD THIS\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n\n $service = new Service(\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService\n );\n\n $config = $this->createMock(Configuration::class);\n\n // Mock profiles relation returning null (no profile found)\n $profilesRelation = \\Mockery::mock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $profilesRelation->shouldReceive('where')->with('crm_provider_id', 'owner-no-profile')->andReturnSelf();\n $profilesRelation->shouldReceive('first')->andReturn(null);\n\n $config->expects($this->once())\n ->method('profiles')\n ->willReturn($profilesRelation);\n\n $team = $this->createMock(Team::class);\n $team->method('getId')->willReturn(1);\n\n $logger = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $logger->expects($this->once())\n ->method('error')\n ->with(\n '[Salesforce] | Skip import, no user_id found',\n ['id' => 'SF-NO-USER-1']\n );\n\n $reflection = new \\ReflectionClass($service);\n\n $configProperty = $reflection->getProperty('config');\n $configProperty->setAccessible(true);\n $configProperty->setValue($service, $config);\n\n $teamProperty = $reflection->getProperty('team');\n $teamProperty->setAccessible(true);\n $teamProperty->setValue($service, $team);\n\n $loggerProperty = $reflection->getProperty('logger');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($service, $logger);\n\n // Initialize profile property to avoid \"must not be accessed before initialization\" error\n $profileProperty = $reflection->getProperty('profile');\n $profileProperty->setAccessible(true);\n $profileProperty->setValue($service, null);\n\n $result = self::invokePrivateMethod('importOpportunity', $service, [$crmData]);\n\n $this->assertNull($result);\n }\n\n public function testImportContactReturnsNullWhenIsDeleted(): void\n {\n $crmData = ['Id' => 'SF-CON-DEL', 'IsDeleted' => true];\n\n $contactsRelation = $this->getMockBuilder(HasMany::class)\n ->disableOriginalConstructor()\n ->addMethods(['where', 'first'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->expects($this->once())->method('contacts')->willReturn($contactsRelation);\n\n $service = $this->getServiceMock(['handleEntityDeletionByProviderId']);\n $service->config = $config;\n\n $service->expects($this->once())\n ->method('handleEntityDeletionByProviderId')\n ->with($contactsRelation, $crmData);\n\n $result = self::invokePrivateMethod('importContact', $service, [$crmData]);\n\n $this->assertNull($result);\n }\n\n public function testImportContactSkipsWritesWhenIsDeleted(): void\n {\n $crmData = ['Id' => 'SF-CON-DEL-2', 'IsDeleted' => true];\n\n $contactsRelation = $this->getMockBuilder(HasMany::class)\n ->disableOriginalConstructor()\n ->addMethods(['where', 'first'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->expects($this->once())->method('contacts')->willReturn($contactsRelation);\n\n $service = $this->getServiceMock(['handleEntityDeletionByProviderId']);\n $service->config = $config;\n\n $service->expects($this->once())->method('handleEntityDeletionByProviderId');\n\n $result = self::invokePrivateMethod('importContact', $service, [$crmData]);\n\n $this->assertNull($result);\n }\n\n public function testImportContactReturnsTrashedContactAsNull(): void\n {\n $crmData = [\n 'Id' => 'SF-CON-TRASHED',\n 'IsDeleted' => false,\n 'OwnerId' => null,\n 'Name' => 'Trashed Contact',\n ];\n\n $contact = $this->createMock(Contact::class);\n $contact->method('trashed')->willReturn(true);\n\n $contactsRelation = $this->getMockBuilder(HasMany::class)\n ->disableOriginalConstructor()\n ->addMethods(['where', 'first', 'withTrashed'])\n ->onlyMethods(['updateOrCreate'])\n ->getMock();\n $contactsRelation->method('where')->willReturnSelf();\n $contactsRelation->method('withTrashed')->willReturnSelf();\n $contactsRelation->method('first')->willReturn(null);\n $contactsRelation->method('updateOrCreate')->willReturn($contact);\n\n $config = $this->createMock(Configuration::class);\n $config->method('contacts')->willReturn($contactsRelation);\n $config->method('accounts')->willReturn($contactsRelation);\n\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $prospectPhotoPathService->method('getOrGeneratePhotoPath')->willReturn('photo.jpg');\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $this->createMock(Client::class),\n $this->createMock(PayloadBuilder::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(CountriesMap::class),\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['handleObjectDeletion'])\n ->getMock();\n\n $service->config = $config;\n $service->profile = $this->createMock(Profile::class);\n\n $team = $this->createMock(Team::class);\n $team->method('getAttribute')->with('id')->willReturn(1);\n $service->team = $team;\n\n $service->method('handleObjectDeletion');\n\n $result = self::invokePrivateMethod('importContact', $service, [$crmData]);\n\n $this->assertNull($result);\n }\n\n public function testImportContactReturnsContactWhenActive(): void\n {\n $crmData = [\n 'Id' => 'SF-CON-ACTIVE',\n 'IsDeleted' => false,\n 'OwnerId' => null,\n 'Name' => 'Active Contact',\n ];\n\n $contact = $this->createMock(Contact::class);\n $contact->method('trashed')->willReturn(false);\n\n $contactsRelation = $this->getMockBuilder(HasMany::class)\n ->disableOriginalConstructor()\n ->addMethods(['where', 'first', 'withTrashed'])\n ->onlyMethods(['updateOrCreate'])\n ->getMock();\n $contactsRelation->method('where')->willReturnSelf();\n $contactsRelation->method('withTrashed')->willReturnSelf();\n $contactsRelation->method('first')->willReturn(null);\n $contactsRelation->method('updateOrCreate')->willReturn($contact);\n\n $config = $this->createMock(Configuration::class);\n $config->method('contacts')->willReturn($contactsRelation);\n $config->method('accounts')->willReturn($contactsRelation);\n\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $prospectPhotoPathService->method('getOrGeneratePhotoPath')->willReturn('photo.jpg');\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $this->createMock(Client::class),\n $this->createMock(PayloadBuilder::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(CountriesMap::class),\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['handleObjectDeletion'])\n ->getMock();\n\n $service->config = $config;\n $service->profile = $this->createMock(Profile::class);\n\n $team = $this->createMock(Team::class);\n $team->method('getAttribute')->with('id')->willReturn(1);\n $service->team = $team;\n\n $service->method('handleObjectDeletion');\n\n $result = self::invokePrivateMethod('importContact', $service, [$crmData]);\n\n $this->assertSame($contact, $result);\n }\n\n public static function resolveContactAccountProvider(): array\n {\n return [\n 'no AccountId returns null' => [[], null],\n 'AccountId present' => [['AccountId' => 'ACC-001'], 'ACC-001'],\n ];\n }\n\n /**\n * @dataProvider resolveContactAccountProvider\n */\n public function testResolveContactAccountWithNoAccountId(array $crmData, ?string $expectedId): void\n {\n $service = $this->getServiceMock(['syncAccount']);\n\n if ($expectedId === null) {\n $service->expects($this->never())->method('syncAccount');\n $config = $this->createMock(Configuration::class);\n $config->expects($this->never())->method('accounts');\n $service->config = $config;\n\n $result = self::invokePrivateMethod('resolveContactAccount', $service, [$crmData]);\n $this->assertNull($result);\n\n return;\n }\n\n $account = $this->createMock(\\Jiminny\\Models\\Account::class);\n\n $accountsRelation = $this->getMockBuilder(HasMany::class)\n ->disableOriginalConstructor()\n ->addMethods(['where', 'first'])\n ->getMock();\n $accountsRelation->method('where')->with('crm_provider_id', $expectedId)->willReturnSelf();\n $accountsRelation->method('first')->willReturn($account);\n\n $config = $this->createMock(Configuration::class);\n $config->method('accounts')->willReturn($accountsRelation);\n $service->config = $config;\n\n $service->expects($this->never())->method('syncAccount');\n\n $result = self::invokePrivateMethod('resolveContactAccount', $service, [$crmData]);\n $this->assertSame($account, $result);\n }\n\n public function testResolveContactAccountSyncsWhenNotFoundLocally(): void\n {\n $syncedAccount = $this->createMock(\\Jiminny\\Models\\Account::class);\n\n $accountsRelation = $this->getMockBuilder(HasMany::class)\n ->disableOriginalConstructor()\n ->addMethods(['where', 'first'])\n ->getMock();\n $accountsRelation->method('where')->willReturnSelf();\n $accountsRelation->method('first')->willReturn(null);\n\n $config = $this->createMock(Configuration::class);\n $config->method('accounts')->willReturn($accountsRelation);\n\n $service = $this->getServiceMock(['syncAccount']);\n $service->config = $config;\n\n $service->expects($this->once())\n ->method('syncAccount')\n ->with('ACC-MISSING')\n ->willReturn($syncedAccount);\n\n $result = self::invokePrivateMethod('resolveContactAccount', $service, [['AccountId' => 'ACC-MISSING']]);\n\n $this->assertSame($syncedAccount, $result);\n }\n\n public static function resolveContactCountryCodeProvider(): array\n {\n return [\n 'valid MailingCountryCode' => [['MailingCountryCode' => 'GB'], true, null, 'GB'],\n 'invalid MailingCountryCode falls to null' => [['MailingCountryCode' => 'XX'], false, null, null],\n 'no code, uses MailingCountry converted' => [['MailingCountry' => 'Germany'], null, 'DE', 'DE'],\n 'no code, country name null, uses account' => [['MailingCountry' => 'Unknown'], null, null, 'US'],\n 'no code, no country at all' => [[], null, null, null],\n ];\n }\n\n /**\n * @dataProvider resolveContactCountryCodeProvider\n */\n public function testResolveContactCountryCode(\n array $crmData,\n ?bool $countryExists,\n ?string $convertedCode,\n ?string $expected\n ): void {\n $countriesMap = $this->createMock(CountriesMap::class);\n if ($countryExists !== null) {\n $countriesMap->method('countryExists')->willReturn($countryExists);\n }\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $this->createMock(Client::class),\n $this->createMock(PayloadBuilder::class),\n $this->createMock(Dispatcher::class),\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods(['convertCountryNameToCode'])\n ->getMock();\n\n $service->profile = $this->createMock(Profile::class);\n\n if (isset($crmData['MailingCountry'])) {\n $service->expects($this->once())\n ->method('convertCountryNameToCode')\n ->with($crmData['MailingCountry'])\n ->willReturn($convertedCode);\n } else {\n $service->expects($this->never())->method('convertCountryNameToCode');\n }\n\n $account = null;\n if ($expected === 'US') {\n $account = new \\Jiminny\\Models\\Account();\n $account->setAttribute('country_code', 'US');\n }\n\n $result = self::invokePrivateMethod('resolveContactCountryCode', $service, [$crmData, $account]);\n\n $this->assertSame($expected, $result);\n }\n\n public static function parseContactPhoneProvider(): array\n {\n return [\n 'empty Phone returns empty' => [['Phone' => ''], null, [[], null]],\n 'no Phone key returns empty' => [[], null, [[], null]],\n ];\n }\n\n /**\n * @dataProvider parseContactPhoneProvider\n */\n public function testParseContactPhoneWithEmptyPhone(array $crmData, ?string $countryCode, array $expected): void\n {\n $service = $this->getServiceMock();\n $result = self::invokePrivateMethod('parseContactPhone', $service, [$countryCode, $crmData]);\n $this->assertSame($expected, $result);\n }\n\n public function testParseContactPhoneWithValidNumber(): void\n {\n $service = $this->getServiceMock();\n $crmData = ['Phone' => '+12025551234'];\n\n [$parsedNumber, $ext] = self::invokePrivateMethod('parseContactPhone', $service, [null, $crmData]);\n\n $this->assertArrayHasKey('phone', $parsedNumber);\n $this->assertSame('+12025551234', $parsedNumber['phone']);\n $this->assertNull($ext);\n }\n\n public function testParseContactPhoneWithExtension(): void\n {\n $service = $this->getServiceMock();\n $crmData = ['Phone' => '+12025551234 ext. 42'];\n\n [$parsedNumber, $ext] = self::invokePrivateMethod('parseContactPhone', $service, ['US', $crmData]);\n\n $this->assertArrayHasKey('phone', $parsedNumber);\n $this->assertSame('42', $ext);\n }\n\n public function testParseContactPhoneExtIsTruncatedTo10Chars(): void\n {\n $service = $this->getServiceMock();\n $crmData = ['Phone' => '+12025551234 ext. 12345678901'];\n\n [$parsedNumber, $ext] = self::invokePrivateMethod('parseContactPhone', $service, ['US', $crmData]);\n\n $this->assertNotNull($ext);\n $this->assertLessThanOrEqual(10, strlen($ext));\n }\n\n public function testParseContactPhoneWithCountryCode(): void\n {\n $service = $this->getServiceMock();\n $crmData = ['Phone' => '02075551234'];\n\n [$parsedNumber, $ext] = self::invokePrivateMethod('parseContactPhone', $service, ['GB', $crmData]);\n\n $this->assertArrayHasKey('phone', $parsedNumber);\n $this->assertStringStartsWith('+44', $parsedNumber['phone']);\n $this->assertNull($ext);\n }\n\n public static function parseContactMobileProvider(): array\n {\n return [\n 'empty MobilePhone returns null' => [['MobilePhone' => ''], null, null],\n 'no MobilePhone key returns null' => [[], null, null],\n ];\n }\n\n /**\n * @dataProvider parseContactMobileProvider\n */\n public function testParseContactMobileWithEmptyPhone(array $crmData, ?string $countryCode, ?string $expected): void\n {\n $service = $this->getServiceMock();\n $result = self::invokePrivateMethod('parseContactMobile', $service, [$countryCode, $crmData]);\n $this->assertSame($expected, $result);\n }\n\n public function testParseContactMobileWithValidNumber(): void\n {\n $service = $this->getServiceMock();\n $crmData = ['MobilePhone' => '+12025550199'];\n\n $result = self::invokePrivateMethod('parseContactMobile', $service, [null, $crmData]);\n\n $this->assertNotNull($result);\n $this->assertSame('+12025550199', $result);\n }\n\n public function testParseContactMobileIsTruncatedTo25Chars(): void\n {\n $service = $this->getServiceMock();\n $longNumber = '+1202555019912345678901234';\n $crmData = ['MobilePhone' => $longNumber];\n\n $result = self::invokePrivateMethod('parseContactMobile', $service, [null, $crmData]);\n\n $this->assertNotNull($result);\n $this->assertLessThanOrEqual(25, strlen($result));\n }\n\n public function testParseContactMobileWithCountryCode(): void\n {\n $service = $this->getServiceMock();\n $crmData = ['MobilePhone' => '07911123456'];\n\n $result = self::invokePrivateMethod('parseContactMobile', $service, ['GB', $crmData]);\n\n $this->assertNotNull($result);\n $this->assertStringStartsWith('+44', $result);\n }\n\n public function testImportOpportunitySkipsWhenProfileNotFound(): void\n {\n $crmData = [\n 'Id' => 'SF-NO-USER-2',\n 'Name' => 'Test Opportunity',\n 'OwnerId' => 'owner-not-found',\n // No AccountId - avoid complex account processing\n ];\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $eventDispatcher = $this->createMock(\\Illuminate\\Events\\Dispatcher::class); // ← ADD THIS\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n\n\n $service = new Service(\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService\n );\n\n $config = $this->createMock(Configuration::class);\n\n // Mock profiles relation returning null (no profile found)\n $profilesRelation = \\Mockery::mock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $profilesRelation->shouldReceive('where')->with('crm_provider_id', 'owner-not-found')->andReturnSelf();\n $profilesRelation->shouldReceive('first')->andReturn(null);\n\n $config->expects($this->once())\n ->method('profiles')\n ->willReturn($profilesRelation);\n\n $team = $this->createMock(Team::class);\n $team->method('getId')->willReturn(1);\n\n $logger = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $logger->expects($this->once())\n ->method('error')\n ->with(\n '[Salesforce] | Skip import, no user_id found',\n ['id' => 'SF-NO-USER-2']\n );\n\n $reflection = new \\ReflectionClass($service);\n\n $configProperty = $reflection->getProperty('config');\n $configProperty->setAccessible(true);\n $configProperty->setValue($service, $config);\n\n $teamProperty = $reflection->getProperty('team');\n $teamProperty->setAccessible(true);\n $teamProperty->setValue($service, $team);\n\n $loggerProperty = $reflection->getProperty('logger');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($service, $logger);\n\n // Initialize profile property\n $profileProperty = $reflection->getProperty('profile');\n $profileProperty->setAccessible(true);\n $profileProperty->setValue($service, null);\n\n $result = self::invokePrivateMethod('importOpportunity', $service, [$crmData]);\n\n $this->assertNull($result);\n }\n}\n\nclass HasManyExtended extends HasMany\n{\n public function where()\n {\n }\n\n public function first()\n {\n }\n}","depth":4,"bounds":{"left":0.13863032,"top":0.12210695,"width":0.3550532,"height":0.87789303},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Crm\\Salesforce;\n\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Events\\Dispatcher;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Jiminny\\Component\\Country\\CountriesMap;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Enums\\CrmObject;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\NoResultsException;\nuse Jiminny\\Integrations\\PlaybookResolver;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\TeamSettings;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Salesforce\\Client;\nuse Jiminny\\Services\\Crm\\Salesforce\\PayloadBuilder;\nuse Jiminny\\Services\\Crm\\Salesforce\\QueryBuilder;\nuse Jiminny\\Services\\Crm\\Salesforce\\QueryHandler;\nuse Jiminny\\Services\\Crm\\Salesforce\\QueryIterator;\nuse Jiminny\\Services\\Crm\\Salesforce\\QueryResults;\nuse Jiminny\\Services\\Crm\\Salesforce\\Service;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse Tests\\TestCase;\nuse Tests\\Unit\\Traits\\TestPrivateMethod;\n\nclass ServiceTest extends TestCase\n{\n use TestPrivateMethod;\n\n public function testFetchAndAssociateRelatedActivity(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $payloadBuilder->method('addCustomLogicFieldsPayload')\n ->willReturnCallback(function ($activity, $payload) {\n return $payload;\n });\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods(['fetchRelatedActivity', 'getPlaybook', 'getPlaybookCategory', 'updateRecord'])\n ->getMock();\n\n $serviceMock->expects($this->once())\n ->method('fetchRelatedActivity')\n ->willReturn([\n 'Id' => 'testId',\n 'Type' => null,\n 'OwnerId' => 'testerUser',\n 'Description' => 'Test description',\n ]);\n\n $user = $this->createMock(User::class);\n $team = $this->createMock(Team::class);\n $user->method('getAttribute')->with('team')->willReturn($team);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityField')->willReturn(null);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_EVENT);\n\n $serviceMock->expects($this->once())\n ->method('getPlaybook')\n ->with($user)\n ->willReturn($playbook);\n\n $serviceMock->expects($this->never())\n ->method('getPlaybookCategory');\n\n $serviceMock->expects($this->never())\n ->method('updateRecord');\n\n $fieldDataRepository = $this->createMock(FieldDataRepository::class);\n $fieldDataRepository->method('getActivityFieldData')->willReturn(collect([]));\n app()->instance(FieldDataRepository::class, $fieldDataRepository);\n\n $config = $this->createMock(Configuration::class);\n $profilesRelation = $this->getMockBuilder(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class)\n ->disableOriginalConstructor()\n ->onlyMethods(['get'])\n ->addMethods(['where'])\n ->getMock();\n $profilesRelation->method('where')->willReturnSelf();\n $profilesRelation->method('get')->willReturn(collect([]));\n $config->method('profiles')->willReturn($profilesRelation);\n\n $serviceMock->config = $config;\n $serviceMock->profile = null;\n\n $actualStartTime = \\Carbon\\Carbon::now();\n\n $activity = $this->getMockBuilder(Activity::class)\n ->disableOriginalConstructor()\n ->onlyMethods(['update', 'hasProspect'])\n ->getMock();\n\n $activity->method('update')->willReturn(true);\n $activity->method('hasProspect')->willReturn(true);\n\n $activity->type = Activity::TYPE_CONFERENCE;\n $activity->provider = Activity::PROVIDER_TWILIO;\n $activity->lead_id = 1;\n $activity->user_id = 0;\n $activity->id_string = 'test-activity-id';\n $activity->user = $user;\n\n $activity->actual_start_time = $actualStartTime;\n $activity->uuid = 'c53d8320-f556-4cee-a2f8-5f232f454ca4';\n\n app()->bind(PlaybookResolver::class, function () use ($user) {\n $playbook = $this->createMock(Playbook::class);\n $playbookResolver = $this->createMock(PlaybookResolver::class);\n $playbookResolver->expects($this->once())\n ->method('resolvePlaybookByUser')\n ->with($user)\n ->willReturn($playbook);\n\n return $playbookResolver;\n });\n\n $data = $serviceMock->fetchAndAssociateRelatedActivity($activity);\n\n $this->assertInstanceOf(Activity::class, $data);\n $this->assertEquals(Activity::TYPE_CONFERENCE, $data->getType());\n $this->assertEquals($actualStartTime->getTimestamp(), $data->getActualStartTime()->getTimestamp());\n }\n\n public function testFetchAndAssociateRelatedActivitySkipsForTaskBasedPlaybook(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods(['fetchRelatedActivity', 'getPlaybook'])\n ->getMock();\n\n $user = $this->createMock(User::class);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n $playbook->method('getId')->willReturn(123);\n\n $serviceMock->expects($this->once())\n ->method('getPlaybook')\n ->with($user)\n ->willReturn($playbook);\n\n $serviceMock->expects($this->never())\n ->method('fetchRelatedActivity');\n\n $activity = $this->getMockBuilder(Activity::class)\n ->disableOriginalConstructor()\n ->onlyMethods(['hasProspect', 'getUuid'])\n ->getMock();\n\n $activity->method('hasProspect')->willReturn(true);\n $activity->method('getUuid')->willReturn('c53d8320-f556-4cee-a2f8-5f232f454ca4');\n $activity->type = Activity::TYPE_CONFERENCE;\n $activity->actual_start_time = \\Carbon\\Carbon::now();\n $activity->user = $user;\n\n $result = $serviceMock->fetchAndAssociateRelatedActivity($activity);\n\n $this->assertNull($result);\n }\n\n public function testFetchAndAssociateRelatedActivityReturnsNullForNonConference(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $serviceMock = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class)\n );\n\n $activity = $this->getMockBuilder(Activity::class)\n ->disableOriginalConstructor()\n ->getMock();\n\n $activity->type = Activity::TYPE_SOFTPHONE;\n\n $result = $serviceMock->fetchAndAssociateRelatedActivity($activity);\n\n $this->assertNull($result);\n }\n\n public function testFetchAndAssociateRelatedActivityReturnsNullWhenNoStartTime(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $serviceMock = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class)\n );\n\n\n $activity = $this->getMockBuilder(Activity::class)\n ->disableOriginalConstructor()\n ->getMock();\n\n $activity->type = Activity::TYPE_CONFERENCE;\n $activity->actual_start_time = null;\n $activity->scheduled_start_time = null;\n\n $result = $serviceMock->fetchAndAssociateRelatedActivity($activity);\n\n $this->assertNull($result);\n }\n\n public function testFetchAndAssociateRelatedActivityReturnsNullWhenNoProspect(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods(['getPlaybook'])\n ->getMock();\n\n $serviceMock->expects($this->never())\n ->method('getPlaybook');\n\n $activity = $this->getMockBuilder(Activity::class)\n ->disableOriginalConstructor()\n ->onlyMethods(['hasProspect', 'getUuid'])\n ->getMock();\n\n $activity->method('hasProspect')->willReturn(false);\n $activity->method('getUuid')->willReturn('c53d8320-f556-4cee-a2f8-5f232f454ca4');\n $activity->type = Activity::TYPE_CONFERENCE;\n $activity->actual_start_time = \\Carbon\\Carbon::now();\n\n $result = $serviceMock->fetchAndAssociateRelatedActivity($activity);\n\n $this->assertNull($result);\n }\n\n public function testMatchExactlyByEmail(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods([])\n ->getMock();\n\n $profile = new Profile();\n $profile->setAttribute('id', bin2hex(random_bytes(8)));\n $serviceMock->profile = $profile;\n\n $team = $this->createMock(Team::class);\n $serviceMock->team = $team;\n\n $data = $serviceMock->matchExactlyByEmail(bin2hex(random_bytes(8)) . 'test_email@testserver.com');\n\n $this->assertEquals(null, $data);\n }\n\n public function testMatchDomainFromEmail(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $queryIterator = $this->createMock(QueryIterator::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $config = $this->createMock(Configuration::class);\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->any())\n ->method('search')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods(['convertCrmData'])\n ->getMock();\n\n $profile = new Profile();\n $profile->account_fields = 'Field1, Field2, Field3';\n $serviceMock->profile = $profile;\n\n $serviceMock->expects($this->once())\n ->method('convertCrmData')\n ->willReturn(['test']);\n\n $this->app->bind(QueryBuilder::class, function () {\n $queryBuilder = $this->createMock(QueryBuilder::class);\n $queryBuilder->expects($this->once())\n ->method('buildMatchByDomainQuery')\n ->with('test_email@testserver.com')\n ->willReturn('FIND {test_email@testserver.com} IN ALL FIELDS RETURNING Account(Id)');\n\n return $queryBuilder;\n });\n\n $team = $this->createMock(Team::class);\n\n $serviceMock->team = $team;\n $serviceMock->config = $config;\n\n $data = $serviceMock->matchByDomain('test_email@testserver.com');\n\n $this->assertEquals(['test'], $data);\n }\n\n public function testBuildTaskSearchFields(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class)\n );\n\n $fields = $service->buildTaskSearchFields();\n\n $expectedFields = ['Id', 'WhoId', 'WhatId', 'AccountId'];\n\n $this->assertEquals($expectedFields, $fields);\n }\n\n public function testMapCrmObjects(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class),\n );\n\n $sampleTask = [\n 'WhoId' => '003sampleWhoId',\n 'AccountId' => 'sampleAccountId',\n 'WhatId' => 'sampleWhatId',\n ];\n\n $activityData = $service->mapCrmObjects($sampleTask);\n\n $expectedActivityData = [\n 'contact' => '003sampleWhoId',\n 'account' => 'sampleAccountId',\n 'opportunity' => 'sampleWhatId',\n ];\n\n $this->assertEquals($expectedActivityData, $activityData);\n }\n\n public function testGetInstalledAppVersion(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n $queryIterator = $this->createMock(QueryIterator::class);\n $queryIterator->expects($this->any())\n ->method('current')->willReturn([\n 'SubscriberPackageVersion' => [\n 'MajorVersion' => '1',\n 'MinorVersion' => '0',\n 'PatchVersion' => '1',\n 'BuildNumber' => '0',\n ],\n ]);\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->any())\n ->method('metadata')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods(array_diff(get_class_methods(Service::class), ['getInstalledAppVersion']))\n ->getMock();\n\n $version = $serviceMock->getInstalledAppVersion();\n\n $this->assertEquals('1010', $version);\n }\n\n public function testSyncProfiles(): void\n {\n $userToSearch = null;\n\n $team = $this->createMock(Team::class);\n $config = $this->createMockedConfiguration();\n $config->expects($this->once())\n ->method('getId')\n ->willReturn(1);\n\n $salesforceUser = [\n 'Email' => 'test@example.com',\n 'UserPreferencesLightningExperiencePreferred' => true,\n 'CallCenterId' => '123',\n 'Id' => '456',\n 'ProfileId' => '789',\n ];\n\n app()->bind(QueryBuilder::class, function () use ($userToSearch) {\n $queryBuilder = $this->createMock(QueryBuilder::class);\n $queryBuilder->expects($this->once())\n ->method('buildGetUsersQuery')\n ->with($userToSearch)\n ->willReturn('SELECT * FROM Users');\n\n return $queryBuilder;\n });\n\n $queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults([$salesforceUser], 1, true, null));\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->any())\n ->method('query')\n ->with('SELECT * FROM Users')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n\n $user = $this->createMock(User::class);\n $user->expects($this->once())\n ->method('getId')\n ->willReturn(123);\n\n $this->mockTeamRepository($team, $salesforceUser, $user);\n\n $profileRepository = $this->createMock(ProfileRepository::class);\n $profileRepository->expects($this->once())\n ->method('updateOrCreateProfile')\n ->with(\n $user,\n [\n 'crm_configuration_id' => 1,\n 'crm_provider_id' => '456',\n ],\n [\n 'user_id' => 123,\n 'edition' => Profile::EDITION_LIGHTNING,\n 'has_external_cti' => true,\n 'crm_profile_id' => '789',\n ]\n )\n ->willReturn(new Profile());\n\n $this->app->instance(ProfileRepository::class, $profileRepository);\n\n $serviceMock = $this->getServiceMock();\n $serviceMock->team = $team;\n $serviceMock->config = $config;\n $result = $serviceMock->syncProfiles($userToSearch);\n\n $this->assertNull($result);\n }\n\n public function testSyncProfilesEmailIsNull(): void\n {\n $userToSearch = $this->createMock(User::class);\n\n $salesforceUser = [\n 'Email' => null,\n ];\n\n app()->bind(QueryBuilder::class, function () {\n $queryBuilder = $this->createMock(QueryBuilder::class);\n $queryBuilder->expects($this->once())\n ->method('buildGetUsersQuery')\n ->with(null)\n ->willReturn('SELECT * FROM Users');\n\n return $queryBuilder;\n });\n\n $queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults([$salesforceUser], 1, true, null));\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->exactly(2))\n ->method('query')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n\n $team = $this->createMock(Team::class);\n $user = $this->createMock(User::class);\n $this->mockTeamRepository($team, $salesforceUser, $user, false);\n\n $config = $this->createMock(Configuration::class);\n\n $serviceMock = $this->getServiceMock();\n $serviceMock->team = $team;\n $serviceMock->config = $config;\n\n $profile = $serviceMock->syncProfiles(null);\n\n $this->assertNull($profile);\n }\n\n public function testSyncProfilesUserToSearchMatchesCurrentUser(): void\n {\n $userToSearch = $this->createMock(User::class);\n $userToSearch->expects($this->once())\n ->method('getId')\n ->willReturn(123);\n\n $team = $this->createMock(Team::class);\n $config = $this->createMock(Configuration::class);\n $config->expects($this->once())\n ->method('getId')\n ->willReturn(1);\n\n $salesforceUser = [\n 'Email' => 'test@example.com',\n 'UserPreferencesLightningExperiencePreferred' => true,\n 'CallCenterId' => '123',\n 'Id' => '456',\n 'ProfileId' => '789',\n ];\n\n app()->bind(QueryBuilder::class, function () use ($userToSearch) {\n $queryBuilder = $this->createMock(QueryBuilder::class);\n $queryBuilder->expects($this->once())\n ->method('buildGetUsersQuery')\n ->with($userToSearch)\n ->willReturn('SELECT * FROM Users');\n\n return $queryBuilder;\n });\n\n $queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults([$salesforceUser], 1, true, null));\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->any())\n ->method('query')\n ->with('SELECT * FROM Users')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n\n $user = $this->createMock(User::class);\n $user->expects($this->exactly(2))\n ->method('getId')\n ->willReturn(123);\n\n $this->mockTeamRepository($team, $salesforceUser, $user);\n\n $profileRepository = $this->createMock(ProfileRepository::class);\n $profileRepository->expects($this->once())\n ->method('updateOrCreateProfile')\n ->with(\n $user,\n [\n 'crm_configuration_id' => 1,\n 'crm_provider_id' => '456',\n ],\n [\n 'user_id' => 123,\n 'edition' => Profile::EDITION_LIGHTNING,\n 'has_external_cti' => true,\n 'crm_profile_id' => '789',\n ]\n )\n ->willReturn(new Profile());\n\n $this->app->instance(ProfileRepository::class, $profileRepository);\n\n $serviceMock = $this->getServiceMock();\n $serviceMock->team = $team;\n $serviceMock->config = $config;\n $profile = $serviceMock->syncProfiles($userToSearch);\n\n $this->assertInstanceOf(Profile::class, $profile);\n }\n\n public function testSyncProfilesWithCustomValidation(): void\n {\n $userToSearch = null;\n\n $team = $this->createMock(Team::class);\n $config = $this->createMockedConfiguration();\n $config->expects($this->atLeastOnce()) // Changed from once() to atLeastOnce()\n ->method('getId')\n ->willReturn(1);\n\n $salesforceUser = [\n 'Email' => 'test@example.com',\n 'UserPreferencesLightningExperiencePreferred' => true,\n 'CallCenterId' => '123',\n 'Id' => '456',\n 'ProfileId' => '789',\n 'CustomField' => 'CustomValue',\n ];\n\n $customRules = [\n ['field' => 'CustomField', 'value' => 'CustomValue'],\n ];\n\n $this->mockQueryBuilderAndHandler($userToSearch, [$salesforceUser]);\n\n $user = $this->createMock(User::class);\n $user->expects($this->once())\n ->method('getId')\n ->willReturn(123);\n\n $this->mockTeamRepository($team, $salesforceUser, $user, true, $customRules);\n\n $this->mockProfileRepository($user);\n\n $serviceMock = $this->getServiceMock();\n $serviceMock->team = $team;\n $serviceMock->config = $config;\n $profile = $serviceMock->syncProfiles($userToSearch);\n\n $this->assertNull($profile);\n }\n\n public function testSyncProfilesWithCustomValidationFailing(): void\n {\n $userToSearch = null;\n\n $team = $this->createMock(Team::class);\n $config = $this->createMockedConfiguration();\n\n $salesforceUser = [\n 'Email' => 'test@example.com',\n 'UserPreferencesLightningExperiencePreferred' => true,\n 'CallCenterId' => '123',\n 'Id' => '456',\n 'ProfileId' => '789',\n 'CustomField' => 'WrongValue',\n ];\n\n $customRules = [\n ['field' => 'CustomField', 'value' => 'CustomValue'],\n ];\n\n $this->mockQueryBuilderAndHandler($userToSearch, [$salesforceUser]);\n\n $teamRepository = $this->getMockForAbstractClass(TeamRepository::class, [], '', false, true, true, ['findActiveTeamMemberByEmail', 'getTeamSetting']);\n\n $teamSettings = $this->createMock(TeamSettings::class);\n $teamSettings->method('getValueType')\n ->willReturn('array');\n\n $teamSettings->method('getValue')\n ->willReturn(json_encode($customRules));\n\n $teamRepository->expects($this->once())\n ->method('getTeamSetting')\n ->with($team, 'custom_profile_validation')\n ->willReturn($teamSettings);\n\n app()->bind(TeamRepository::class, function () use ($teamRepository) {\n return $teamRepository;\n });\n\n $profileRepository = $this->createMock(ProfileRepository::class);\n $profileRepository->expects($this->never())\n ->method('updateOrCreateProfile');\n\n $this->app->instance(ProfileRepository::class, $profileRepository);\n\n $serviceMock = $this->getServiceMock();\n $serviceMock->team = $team;\n $serviceMock->config = $config;\n $result = $serviceMock->syncProfiles($userToSearch);\n\n $this->assertNull($result);\n }\n\n public function testGetContactRolesFromCrm(): void\n {\n $contactRoles = [\n [\n 'Id' => '1',\n 'ContactId' => 'Contact1',\n 'OpportunityId' => 'Opportunity1',\n 'Opportunity' => ['OwnerId' => 'Owner1'],\n 'IsPrimary' => true,\n 'Role' => 'Decision Maker',\n ],\n ];\n\n $expectedResponse = [\n [\n 'id' => '1',\n 'contactId' => 'Contact1',\n 'opportunityId' => 'Opportunity1',\n 'ownerId' => 'Owner1',\n 'isPrimary' => true,\n 'role' => 'Decision Maker',\n ],\n ];\n\n $this->bindQueryIterator($contactRoles);\n\n $serviceMock = $this->getServiceMock();\n\n $data = $serviceMock->getContactRolesFromCrm(now()->subDay());\n\n $this->assertEquals($expectedResponse, $data);\n }\n\n public function testGetContactRolesFromCrmNoResult(): void\n {\n $this->bindQueryIterator([]);\n\n $serviceMock = $this->getServiceMock();\n\n $data = $serviceMock->getContactRolesFromCrm(now()->subDay());\n\n $this->assertEquals([], $data);\n }\n\n public function testSyncContactRoles(): void\n {\n $contactRoles = [\n [\n 'id' => '1',\n 'contactId' => 'Contact1',\n 'opportunityId' => 'Opportunity1',\n 'ownerId' => 'Owner1',\n 'isPrimary' => true,\n 'role' => 'Decision Maker',\n ],\n ];\n\n app()->bind(ContactRoleRepository::class, function () {\n $contactRoleRepository = $this->createMock(ContactRoleRepository::class);\n $contactRoleRepository->expects($this->once())\n ->method('saveContactRoles');\n\n return $contactRoleRepository;\n });\n\n $serviceMock = $this->getServiceMock([\n 'getContactRolesFromCrm',\n 'syncRemotelyDeletedContactRoles',\n 'syncContact',\n 'syncOpportunity',\n ]);\n\n $config = $this->createMock(Configuration::class);\n $hasMany = $this->createMock(HasManyExtended::class);\n $hasMany->expects($this->exactly(2))\n ->method('where')\n ->willReturn($hasMany);\n\n $hasMany->expects($this->exactly(2))\n ->method('first')\n ->willReturn(\n $this->createMock(Contact::class),\n $this->createMock(Opportunity::class)\n );\n\n $config->expects($this->once())\n ->method('contacts')\n ->willReturn($hasMany);\n $config->expects($this->once())\n ->method('opportunities')\n ->willReturn($hasMany);\n\n $serviceMock->config = $config;\n\n $serviceMock->expects($this->once())\n ->method('getContactRolesFromCrm')\n ->willReturn($contactRoles);\n\n $serviceMock->expects($this->once())\n ->method('syncRemotelyDeletedContactRoles');\n\n $data = $serviceMock->syncContactRoles(now()->subDay());\n\n $this->assertEquals(1, $data);\n }\n\n public function testSyncRemotelyDeletedContactRoles(): void\n {\n $contactRoles = [\n [\n 'id' => '1',\n 'crm_provider_id' => '1',\n ],\n ];\n\n app()->bind(QueryHandler::class, function () use ($contactRoles) {\n $queryResults = new QueryResults($contactRoles, 1, true, null);\n\n $handler = $this->createMock(QueryHandler::class);\n $handler->method('queryDeleted')\n ->willReturn($queryResults);\n\n return $handler;\n });\n\n app()->bind(ContactRoleRepository::class, function () {\n $contactRoleRepository = $this->createMock(ContactRoleRepository::class);\n $contactRoleRepository->expects($this->once())\n ->method('deleteContactRoles');\n\n return $contactRoleRepository;\n });\n\n $serviceMock = $this->getServiceMock();\n $serviceMock->team = $this->createMock(Team::class);\n\n $data = $this->invokePrivateMethod('syncRemotelyDeletedContactRoles', $serviceMock, []);\n\n $this->assertTrue($data);\n }\n\n private function bindQueryIterator(array $queryResult): void\n {\n /** @var Client $client */\n $client = $this->createMock(Client::class);\n $queryIterator = new QueryIterator(\n $client,\n new QueryResults($queryResult, 1, true, null)\n );\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->any())\n ->method('query')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n }\n\n public static function getOpportunitySortOrderDataProvider(): array\n {\n return [\n 'all open recently updated' => [Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED, ['LastModifiedDate DESC', true]],\n 'all open recently created' => [Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED, ['CreatedDate DESC', true]],\n 'all open oldest created' => [Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED, ['CreatedDate ASC', true]],\n 'all recently updated' => [Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED, ['LastModifiedDate DESC', false]],\n 'default' => ['unknown', ['LastModifiedDate DESC', true]],\n ];\n }\n\n private function createMockedConfiguration(): Configuration\n {\n $config = $this->createMock(Configuration::class);\n $profilesRelation = $this->getMockBuilder(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class)\n ->disableOriginalConstructor()\n ->onlyMethods(['get'])\n ->addMethods(['where', 'first'])\n ->getMock();\n $profilesRelation->method('where')->willReturnSelf();\n $profilesRelation->method('get')->willReturn(collect([]));\n $profilesRelation->method('first')->willReturn(null);\n $config->method('profiles')->willReturn($profilesRelation);\n\n return $config;\n }\n\n private function getServiceMock(array $onlyMethods = []): MockObject&Service\n {\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $this->createMock(Client::class),\n $this->createMock(PayloadBuilder::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(CountriesMap::class),\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods($onlyMethods)\n ->getMock();\n\n $serviceMock->profile = $this->createMock(Profile::class);\n\n return $serviceMock;\n }\n\n private function mockQueryBuilderAndHandler($userToSearch, $salesforceUsers): void\n {\n app()->bind(QueryBuilder::class, function () use ($userToSearch) {\n $queryBuilder = $this->createMock(QueryBuilder::class);\n $queryBuilder->expects($this->once())\n ->method('buildGetUsersQuery')\n ->with($userToSearch)\n ->willReturn('SELECT * FROM Users');\n\n return $queryBuilder;\n });\n\n $queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults($salesforceUsers, count($salesforceUsers), true, null));\n\n app()->bind(QueryHandler::class, function () use ($queryIterator) {\n $handler = $this->createMock(QueryHandler::class);\n $handler->expects($this->any())\n ->method('query')\n ->willReturn($queryIterator);\n\n return $handler;\n });\n }\n\n private function mockTeamRepository(\n Team $team,\n array $salesforceUser,\n ?User $user = null,\n bool $userSearch = true,\n array $customRules = []\n ): void {\n $teamRepository = $this->getMockForAbstractClass(TeamRepository::class, [], '', false, true, true, ['findActiveTeamMemberByEmail', 'getTeamSetting']);\n\n if ($userSearch) {\n $teamRepository->expects($this->once())\n ->method('findActiveTeamMemberByEmail')\n ->with($team, $salesforceUser['Email'])\n ->willReturn($user);\n }\n\n $teamSettings = $this->createMock(TeamSettings::class);\n $teamSettings->method('getValueType')\n ->willReturn('array');\n\n $teamSettings->method('getValue')\n ->willReturn(json_encode($customRules));\n\n $teamRepository->expects($this->once())\n ->method('getTeamSetting')\n ->with($team, 'custom_profile_validation')\n ->willReturn($teamSettings);\n\n app()->bind(TeamRepository::class, function () use ($teamRepository) {\n return $teamRepository;\n });\n }\n\n private function mockProfileRepository(User $user): void\n {\n $profileRepository = $this->createMock(ProfileRepository::class);\n $profileRepository->expects($this->once())\n ->method('updateOrCreateProfile')\n ->with(\n $user,\n [\n 'crm_configuration_id' => 1,\n 'crm_provider_id' => '456',\n ],\n [\n 'user_id' => 123,\n 'edition' => Profile::EDITION_LIGHTNING,\n 'has_external_cti' => true,\n 'crm_profile_id' => '789',\n ]\n )\n ->willReturn(new Profile());\n\n $this->app->instance(ProfileRepository::class, $profileRepository);\n }\n\n public function testBuildEnhancedNoteDecodesHtmlEntities(): void\n {\n $service = $this->getServiceMock(['createRecord']);\n\n $profile = new Profile();\n $profile->setAttribute('crm_provider_id', 'owner-123');\n $service->profile = $profile;\n\n $service->expects($this->exactly(2))\n ->method('createRecord')\n ->willReturnOnConsecutiveCalls('note-id-123', 'link-id-456');\n\n $bodyWithEntities = 'Welch's current challenges and Facebook's Club';\n\n $result = $this->invokePrivateMethod('buildEnhancedNote', $service, [\n 'Test Title',\n $bodyWithEntities,\n 'object-id-789',\n ]);\n\n $this->assertEquals('note-id-123', $result);\n }\n\n public function testBuildEnhancedNoteSanitizesWithoutQuotes(): void\n {\n $service = $this->getServiceMock(['createRecord']);\n\n $profile = new Profile();\n $profile->setAttribute('crm_provider_id', 'owner-456');\n $service->profile = $profile;\n\n $service->expects($this->exactly(2))\n ->method('createRecord')\n ->willReturnCallback(function ($type, $data) {\n if ($type === 'ContentNote') {\n $decoded = base64_decode($data['Content']);\n $this->assertStringContainsString(\"Welch's\", $decoded);\n $this->assertStringNotContainsString(''', $decoded);\n $this->assertStringNotContainsString('&#039;', $decoded);\n $this->assertStringContainsString('<script>', $decoded);\n\n return 'note-id-456';\n }\n\n return 'link-id-789';\n });\n\n $bodyWithMixedContent = \"Welch's and <script>alert('xss')</script>\";\n\n $result = $this->invokePrivateMethod('buildEnhancedNote', $service, [\n 'Test Title',\n $bodyWithMixedContent,\n 'object-id-123',\n ]);\n\n $this->assertEquals('note-id-456', $result);\n }\n\n public function testBuildEnhancedNoteConvertsLineBreaks(): void\n {\n $service = $this->getServiceMock(['createRecord']);\n\n $profile = new Profile();\n $profile->setAttribute('crm_provider_id', 'owner-789');\n $service->profile = $profile;\n\n $service->expects($this->exactly(2))\n ->method('createRecord')\n ->willReturnCallback(function ($type, $data) {\n if ($type === 'ContentNote') {\n $decoded = base64_decode($data['Content']);\n $this->assertStringContainsString('<br>', $decoded);\n $this->assertStringNotContainsString('<br />', $decoded);\n\n return 'note-id-789';\n }\n\n return 'link-id-012';\n });\n\n $bodyWithLineBreaks = \"Line 1\\nLine 2\\nLine 3\";\n\n $result = $this->invokePrivateMethod('buildEnhancedNote', $service, [\n 'Test Title',\n $bodyWithLineBreaks,\n 'object-id-456',\n ]);\n\n $this->assertEquals('note-id-789', $result);\n }\n\n public function testBuildEnhancedNoteHandlesComplexScenario(): void\n {\n $service = $this->getServiceMock(['createRecord']);\n\n $profile = new Profile();\n $profile->setAttribute('crm_provider_id', 'owner-complex');\n $service->profile = $profile;\n\n $service->expects($this->exactly(2))\n ->method('createRecord')\n ->willReturnCallback(function ($type, $data) {\n if ($type === 'ContentNote') {\n $decoded = base64_decode($data['Content']);\n\n $this->assertStringContainsString(\"Welch's\", $decoded);\n $this->assertStringContainsString(\"Facebook's Club\", $decoded);\n $this->assertStringContainsString(\"Arctics'\", $decoded);\n $this->assertStringNotContainsString(''', $decoded);\n $this->assertStringNotContainsString('&#039;', $decoded);\n $this->assertStringContainsString('<br>', $decoded);\n $this->assertStringContainsString('<', $decoded);\n $this->assertStringContainsString('>', $decoded);\n\n return 'note-complex';\n }\n\n return 'link-complex';\n });\n\n $complexBody = \"Summary:\\n---------\\nThe call focused on understanding Welch's current challenges and exploring how Arctics' Revenue Growth Management solutions could support their strategic goals.\\n\\n• John SMith discussed his role as a category advisor for Google and Facebook's Club, emphasizing the importance of market research and advising on product assortment.\\n• Madona introduced Arctics' Virtual Shoppers AI, which simulates consumer <behavior> to optimize pricing and promotional strategies.\";\n\n $result = $this->invokePrivateMethod('buildEnhancedNote', $service, [\n 'Jiminny Transcription Summary',\n $complexBody,\n 'task-id-001',\n ]);\n\n $this->assertEquals('note-complex', $result);\n }\n\n public function testSyncRemotelyDeletedObjectsWithErrorHandlingSuccess(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['syncRemotelyDeletedObjects'])\n ->getMock();\n\n // Mock team\n $team = $this->createMock(Team::class);\n $team->method('getUuid')->willReturn('team-uuid-123');\n $serviceMock->team = $team;\n\n // Expect syncRemotelyDeletedObjects to be called once and succeed\n $serviceMock->expects($this->once())\n ->method('syncRemotelyDeletedObjects')\n ->with(\\Jiminny\\Enums\\CrmObject::ACCOUNT);\n\n // Call the protected method using reflection\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');\n $method->setAccessible(true);\n\n // Should not throw any exceptions\n $method->invoke($serviceMock, \\Jiminny\\Enums\\CrmObject::ACCOUNT);\n\n $this->assertTrue(true); // Test completed successfully\n }\n\n public function testSyncRemotelyDeletedObjectsWithErrorHandlingFailure(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['syncRemotelyDeletedObjects'])\n ->getMock();\n\n // Mock team\n $team = $this->createMock(Team::class);\n $team->method('getUuid')->willReturn('team-uuid-456');\n $serviceMock->team = $team;\n\n // Mock logger to verify warning is logged\n $logger = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n\n // Use reflection to set the protected logger property\n $reflection = new \\ReflectionClass($serviceMock);\n $loggerProperty = $reflection->getProperty('logger');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($serviceMock, $logger);\n\n $exception = new \\Exception('Sync failed due to API error');\n\n // Expect syncRemotelyDeletedObjects to throw an exception\n $serviceMock->expects($this->once())\n ->method('syncRemotelyDeletedObjects')\n ->with(\\Jiminny\\Enums\\CrmObject::CONTACT)\n ->willThrowException($exception);\n\n // Expect warning to be logged with correct message and parameters\n $logger->expects($this->once())\n ->method('warning')\n ->with(\n '[Salesforce] Remotely deleted objects sync failed',\n [\n 'objectType' => 'contact',\n 'teamId' => 'team-uuid-456',\n 'reason' => 'Sync failed due to API error',\n ]\n );\n\n // Call the protected method using reflection\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');\n $method->setAccessible(true);\n\n // Should not re-throw the exception, just log it\n $method->invoke($serviceMock, \\Jiminny\\Enums\\CrmObject::CONTACT);\n\n $this->assertTrue(true); // Test completed successfully\n }\n\n public function testSyncRemotelyDeletedObjectsWithErrorHandlingWithLogParams(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['syncRemotelyDeletedObjects'])\n ->getMock();\n\n // Mock team\n $team = $this->createMock(Team::class);\n $team->method('getUuid')->willReturn('team-uuid-789');\n $serviceMock->team = $team;\n\n // Mock logger to verify warning is logged\n $logger = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n\n // Use reflection to set the protected logger property\n $loggerReflection = new \\ReflectionClass($serviceMock);\n $loggerProperty = $loggerReflection->getProperty('logger');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($serviceMock, $logger);\n\n $exception = new \\Exception('Network timeout');\n\n // Expect syncRemotelyDeletedObjects to throw an exception\n $serviceMock->expects($this->once())\n ->method('syncRemotelyDeletedObjects')\n ->with(\\Jiminny\\Enums\\CrmObject::OPPORTUNITY)\n ->willThrowException($exception);\n\n // Additional log parameters\n $logParams = [\n 'syncType' => 'full',\n 'batchSize' => 100,\n ];\n\n // Expect warning to be logged with merged parameters\n $logger->expects($this->once())\n ->method('warning')\n ->with(\n '[Salesforce] Remotely deleted objects sync failed',\n [\n 'objectType' => 'opportunity',\n 'teamId' => 'team-uuid-789',\n 'reason' => 'Network timeout',\n 'syncType' => 'full',\n 'batchSize' => 100,\n ]\n );\n\n // Call the protected method using reflection\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');\n $method->setAccessible(true);\n\n // Should not re-throw the exception, just log it\n $method->invoke($serviceMock, \\Jiminny\\Enums\\CrmObject::OPPORTUNITY, $logParams);\n\n $this->assertTrue(true); // Test completed successfully\n }\n\n /**\n * @dataProvider crmObjectProvider\n */\n public function testSyncRemotelyDeletedObjectsWithErrorHandlingDifferentCrmObjects(\\Jiminny\\Enums\\CrmObject $crmObject): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['syncRemotelyDeletedObjects'])\n ->getMock();\n\n // Mock team\n $team = $this->createMock(Team::class);\n $team->method('getUuid')->willReturn('team-uuid-test');\n $serviceMock->team = $team;\n\n // Mock logger to verify warning is logged\n $logger = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n\n // Use reflection to set the protected logger property\n $loggerReflectionClass = new \\ReflectionClass($serviceMock);\n $loggerProperty = $loggerReflectionClass->getProperty('logger');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($serviceMock, $logger);\n\n $exception = new \\Exception('Test error');\n\n // Expect syncRemotelyDeletedObjects to throw an exception\n $serviceMock->expects($this->once())\n ->method('syncRemotelyDeletedObjects')\n ->with($crmObject)\n ->willThrowException($exception);\n\n // Expect warning to be logged with correct entity type\n $logger->expects($this->once())\n ->method('warning')\n ->with(\n '[Salesforce] Remotely deleted objects sync failed',\n [\n 'objectType' => $crmObject->value,\n 'teamId' => 'team-uuid-test',\n 'reason' => 'Test error',\n ]\n );\n\n // Call the protected method using reflection\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');\n $method->setAccessible(true);\n\n $method->invoke($serviceMock, $crmObject);\n\n $this->assertTrue(true); // Test completed successfully\n }\n\n public function testHandleObjectDeletionWithDeletedEntity(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['deleteCrmObject'])\n ->getMock();\n\n $entity = $this->createMock(\\Jiminny\\Models\\Account::class);\n $crmData = ['IsDeleted' => true];\n\n $serviceMock->expects($this->once())\n ->method('deleteCrmObject')\n ->with($entity);\n\n // Use reflection to call the protected method\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('handleObjectDeletion');\n $method->setAccessible(true);\n\n $method->invoke($serviceMock, $entity, $crmData);\n }\n\n public function testHandleObjectDeletionWithNonDeletedEntity(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['deleteCrmObject'])\n ->getMock();\n\n $entity = $this->createMock(\\Jiminny\\Models\\Contact::class);\n $crmData = ['IsDeleted' => false];\n\n $serviceMock->expects($this->never())\n ->method('deleteCrmObject');\n\n // Use reflection to call the protected method\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('handleObjectDeletion');\n $method->setAccessible(true);\n\n $method->invoke($serviceMock, $entity, $crmData);\n }\n\n public function testDeleteCrmObjectWithValidEntity(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['dispatchDeleteCrmObjectJob'])\n ->getMock();\n\n $entity = $this->createMock(\\Jiminny\\Models\\Lead::class);\n $entity->expects($this->once())->method('delete');\n\n $serviceMock->expects($this->once())\n ->method('dispatchDeleteCrmObjectJob')\n ->with($entity);\n\n // Use reflection to call the protected method\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('deleteCrmObject');\n $method->setAccessible(true);\n\n $method->invoke($serviceMock, $entity);\n }\n\n public function testDeleteCrmObjectWithNullEntity(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n /** @var Service&MockObject $serviceMock */\n $serviceMock = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['dispatchDeleteCrmObjectJob'])\n ->getMock();\n\n $serviceMock->expects($this->never())\n ->method('dispatchDeleteCrmObjectJob');\n\n // Use reflection to call the protected method\n $reflection = new \\ReflectionClass($serviceMock);\n $method = $reflection->getMethod('deleteCrmObject');\n $method->setAccessible(true);\n\n $method->invoke($serviceMock, null);\n }\n\n public function testDispatchDeleteCrmObjectJobWithNullEntity(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $prospectPhotoPathService,\n );\n\n // Use reflection to call the protected method\n $reflection = new \\ReflectionClass($service);\n $method = $reflection->getMethod('dispatchDeleteCrmObjectJob');\n $method->setAccessible(true);\n\n // Should return early without dispatching - no exception expected\n $method->invoke($service, null);\n\n $this->assertTrue(true); // Test completed successfully\n }\n\n public function testDispatchDeleteCrmObjectJobWithUnsupportedEntity(): void\n {\n $this->expectException(\\TypeError::class);\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $prospectPhotoPathService,\n );\n\n $unsupportedEntity = $this->createMock(\\stdClass::class);\n\n // Use reflection to call the protected method\n $reflection = new \\ReflectionClass($service);\n $method = $reflection->getMethod('dispatchDeleteCrmObjectJob');\n\n // This will throw TypeError due to union type constraint\n $method->invoke($service, $unsupportedEntity);\n }\n\n public function testHandleEntityDeletionByProviderIdMethodExists(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $prospectPhotoPathService,\n );\n\n // Test that the method exists and is accessible via reflection\n $reflection = new \\ReflectionClass($service);\n $method = $reflection->getMethod('handleEntityDeletionByProviderId');\n $method->setAccessible(true);\n\n // Verify method exists and has correct parameters\n $this->assertTrue($method->isProtected());\n $this->assertEquals(2, $method->getNumberOfParameters());\n\n $parameters = $method->getParameters();\n $this->assertEquals('targetEntity', $parameters[0]->getName());\n $this->assertEquals('crmData', $parameters[1]->getName());\n }\n\n public function testSyncRemotelyDeletedObjectsWithNoResults(): void\n {\n // Create a real service instance to avoid mock property issues\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $prospectPhotoPathService,\n );\n\n // Mock queryHandler to throw NoResultsException\n $queryHandler = $this->createMock(QueryHandler::class);\n $queryHandler->expects($this->once())\n ->method('queryDeleted')\n ->with('Opportunity')\n ->willThrowException(new NoResultsException('No results'));\n\n // Set the queryHandler using reflection on the real service\n $reflection = new \\ReflectionClass($service);\n $queryHandlerProperty = $reflection->getProperty('queryHandler');\n $queryHandlerProperty->setAccessible(true);\n $queryHandlerProperty->setValue($service, $queryHandler);\n\n $result = self::invokePrivateMethod('syncRemotelyDeletedObjects', $service, [CrmObject::OPPORTUNITY]);\n\n $this->assertFalse($result);\n }\n\n public function testSyncRemotelyDeletedObjectsWithEmptyResults(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $prospectPhotoPathService,\n );\n\n // Mock queryHandler to return empty results\n $queryResult = $this->createMock(QueryResults::class);\n $queryResult->method('getResults')->willReturn([]);\n\n $queryHandler = $this->createMock(QueryHandler::class);\n $queryHandler->expects($this->once())\n ->method('queryDeleted')\n ->with('Opportunity')\n ->willReturn($queryResult);\n\n // Set the queryHandler using reflection on the real service\n $reflection = new \\ReflectionClass($service);\n $queryHandlerProperty = $reflection->getProperty('queryHandler');\n $queryHandlerProperty->setAccessible(true);\n $queryHandlerProperty->setValue($service, $queryHandler);\n\n $result = self::invokePrivateMethod('syncRemotelyDeletedObjects', $service, [CrmObject::OPPORTUNITY]);\n\n $this->assertFalse($result);\n }\n\n public function testSyncRemotelyDeletedObjectsWithUnsupportedCrmObject(): void\n {\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = new Service(\n client: $client,\n payloadBuilder: $payloadBuilder,\n eventDispatcher: $eventDispatcher,\n countriesMap: $countriesMap,\n prospectPhotoPathService: $prospectPhotoPathService,\n );\n\n // Mock queryHandler to return some deleted objects so we reach the match statement\n $deletedObjects = [\n ['id' => 'task1'],\n ['id' => 'task2'],\n ];\n $queryResult = $this->createMock(QueryResults::class);\n $queryResult->method('getResults')->willReturn($deletedObjects);\n\n $queryHandler = $this->createMock(QueryHandler::class);\n $queryHandler->expects($this->once())\n ->method('queryDeleted')\n ->with('Task') // ucfirst('task') = 'Task'\n ->willReturn($queryResult);\n\n self::setPrivateProperty($service, 'queryHandler', $queryHandler);\n\n $this->expectException(InvalidArgumentException::class);\n $this->expectExceptionMessage('Unsupported CrmObject: task');\n\n self::invokePrivateMethod('syncRemotelyDeletedObjects', $service, [CrmObject::TASK]);\n }\n\n public static function crmObjectProvider(): array\n {\n return [\n 'Account' => [CrmObject::ACCOUNT],\n 'Contact' => [CrmObject::CONTACT],\n 'Lead' => [CrmObject::LEAD],\n 'Opportunity' => [CrmObject::OPPORTUNITY],\n ];\n }\n\n public function testVerifyTaskExistsReturnsTrue(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:task-123', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-123');\n $activity->method('getId')->willReturn(456);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Task', 'task-123', ['Id', 'IsDeleted'])\n ->willReturn(['Id' => 'task-123', 'IsDeleted' => false]);\n\n $result = $service->verifyTaskExists($activity);\n\n $this->assertTrue($result);\n }\n\n public function testVerifyTaskExistsReturnsTrueForEvent(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:event-123', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('event-123');\n $activity->method('getId')->willReturn(456);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_EVENT);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Event', 'event-123', ['Id', 'IsDeleted'])\n ->willReturn(['Id' => 'event-123', 'IsDeleted' => false]);\n\n $result = $service->verifyTaskExists($activity);\n\n $this->assertTrue($result);\n }\n\n public function testVerifyTaskExistsReturnsFalseWhenDeleted(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:task-456', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-456');\n $activity->method('getId')->willReturn(789);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Task', 'task-456', ['Id', 'IsDeleted'])\n ->willReturn(['Id' => 'task-456', 'IsDeleted' => true]);\n\n $result = $service->verifyTaskExists($activity);\n\n $this->assertFalse($result);\n }\n\n public function testVerifyTaskExistsReturnsFalseWhenNotFound(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:task-999', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-999');\n $activity->method('getId')->willReturn(999);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Task', 'task-999', ['Id', 'IsDeleted'])\n ->willThrowException(new \\Jiminny\\Exceptions\\HttpNotFoundException('Task not found'));\n\n $result = $service->verifyTaskExists($activity);\n\n $this->assertFalse($result);\n }\n\n public function testVerifyTaskExistsReturnsFalseWhenNoPlaybook(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:task-no-playbook', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-no-playbook');\n $activity->method('getId')->willReturn(111);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn(null);\n\n $result = $service->verifyTaskExists($activity);\n\n $this->assertFalse($result);\n }\n\n public function testVerifyTaskExistsThrowsExceptionForTransientErrors(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:task-error', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-error');\n $activity->method('getId')->willReturn(888);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Task', 'task-error', ['Id', 'IsDeleted'])\n ->willThrowException(new \\RuntimeException('Network timeout'));\n\n $this->expectException(\\RuntimeException::class);\n $this->expectExceptionMessage('Network timeout');\n\n $service->verifyTaskExists($activity);\n }\n\n public function testVerifyTaskExistsCachesResults(): void\n {\n $cachedValue = null;\n Cache::shouldReceive('remember')\n ->twice()\n ->with('crm_task_exists:123:task-cached', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(function ($key, $ttl, $callback) use (&$cachedValue) {\n if ($cachedValue === null) {\n $cachedValue = $callback();\n }\n\n return $cachedValue;\n });\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-cached');\n $activity->method('getId')->willReturn(555);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Task', 'task-cached', ['Id', 'IsDeleted'])\n ->willReturn(['Id' => 'task-cached', 'IsDeleted' => false]);\n\n $result1 = $service->verifyTaskExists($activity);\n $result2 = $service->verifyTaskExists($activity);\n\n $this->assertTrue($result1);\n $this->assertTrue($result2);\n }\n\n public function testVerifyTaskExistsReturnsFalseForHttpBadRequestException(): void\n {\n Cache::shouldReceive('remember')\n ->once()\n ->with('crm_task_exists:123:task-400', 86400, \\Mockery::type('Closure'))\n ->andReturnUsing(fn ($key, $ttl, $callback) => $callback());\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $eventDispatcher = $this->createMock(Dispatcher::class);\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['getRecord', 'getPlaybookFromActivity'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->method('getId')->willReturn(123);\n\n self::setPrivateProperty($service, 'config', $config);\n\n $activity = $this->createMock(Activity::class);\n $activity->method('getCrmProviderId')->willReturn('task-400');\n $activity->method('getId')->willReturn(400);\n\n $playbook = $this->createMock(Playbook::class);\n $playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);\n\n $service->expects($this->once())\n ->method('getPlaybookFromActivity')\n ->with($activity)\n ->willReturn($playbook);\n\n $service->expects($this->once())\n ->method('getRecord')\n ->with('Task', 'task-400', ['Id', 'IsDeleted'])\n ->willThrowException(new \\Jiminny\\Exceptions\\HttpBadRequestException('Bad request'));\n\n $result = $service->verifyTaskExists($activity);\n\n $this->assertFalse($result);\n }\n\n public function testImportOpportunitySkipsWhenNoProfileAndNoAccount(): void\n {\n $crmData = [\n 'Id' => 'SF-NO-USER-1',\n 'Name' => 'Test Opportunity',\n 'OwnerId' => 'owner-no-profile',\n // No AccountId\n ];\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $countriesMap = $this->createMock(CountriesMap::class);\n $eventDispatcher = $this->createMock(\\Illuminate\\Events\\Dispatcher::class); // ← ADD THIS\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n\n $service = new Service(\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService\n );\n\n $config = $this->createMock(Configuration::class);\n\n // Mock profiles relation returning null (no profile found)\n $profilesRelation = \\Mockery::mock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $profilesRelation->shouldReceive('where')->with('crm_provider_id', 'owner-no-profile')->andReturnSelf();\n $profilesRelation->shouldReceive('first')->andReturn(null);\n\n $config->expects($this->once())\n ->method('profiles')\n ->willReturn($profilesRelation);\n\n $team = $this->createMock(Team::class);\n $team->method('getId')->willReturn(1);\n\n $logger = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $logger->expects($this->once())\n ->method('error')\n ->with(\n '[Salesforce] | Skip import, no user_id found',\n ['id' => 'SF-NO-USER-1']\n );\n\n $reflection = new \\ReflectionClass($service);\n\n $configProperty = $reflection->getProperty('config');\n $configProperty->setAccessible(true);\n $configProperty->setValue($service, $config);\n\n $teamProperty = $reflection->getProperty('team');\n $teamProperty->setAccessible(true);\n $teamProperty->setValue($service, $team);\n\n $loggerProperty = $reflection->getProperty('logger');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($service, $logger);\n\n // Initialize profile property to avoid \"must not be accessed before initialization\" error\n $profileProperty = $reflection->getProperty('profile');\n $profileProperty->setAccessible(true);\n $profileProperty->setValue($service, null);\n\n $result = self::invokePrivateMethod('importOpportunity', $service, [$crmData]);\n\n $this->assertNull($result);\n }\n\n public function testImportContactReturnsNullWhenIsDeleted(): void\n {\n $crmData = ['Id' => 'SF-CON-DEL', 'IsDeleted' => true];\n\n $contactsRelation = $this->getMockBuilder(HasMany::class)\n ->disableOriginalConstructor()\n ->addMethods(['where', 'first'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->expects($this->once())->method('contacts')->willReturn($contactsRelation);\n\n $service = $this->getServiceMock(['handleEntityDeletionByProviderId']);\n $service->config = $config;\n\n $service->expects($this->once())\n ->method('handleEntityDeletionByProviderId')\n ->with($contactsRelation, $crmData);\n\n $result = self::invokePrivateMethod('importContact', $service, [$crmData]);\n\n $this->assertNull($result);\n }\n\n public function testImportContactSkipsWritesWhenIsDeleted(): void\n {\n $crmData = ['Id' => 'SF-CON-DEL-2', 'IsDeleted' => true];\n\n $contactsRelation = $this->getMockBuilder(HasMany::class)\n ->disableOriginalConstructor()\n ->addMethods(['where', 'first'])\n ->getMock();\n\n $config = $this->createMock(Configuration::class);\n $config->expects($this->once())->method('contacts')->willReturn($contactsRelation);\n\n $service = $this->getServiceMock(['handleEntityDeletionByProviderId']);\n $service->config = $config;\n\n $service->expects($this->once())->method('handleEntityDeletionByProviderId');\n\n $result = self::invokePrivateMethod('importContact', $service, [$crmData]);\n\n $this->assertNull($result);\n }\n\n public function testImportContactReturnsTrashedContactAsNull(): void\n {\n $crmData = [\n 'Id' => 'SF-CON-TRASHED',\n 'IsDeleted' => false,\n 'OwnerId' => null,\n 'Name' => 'Trashed Contact',\n ];\n\n $contact = $this->createMock(Contact::class);\n $contact->method('trashed')->willReturn(true);\n\n $contactsRelation = $this->getMockBuilder(HasMany::class)\n ->disableOriginalConstructor()\n ->addMethods(['where', 'first', 'withTrashed'])\n ->onlyMethods(['updateOrCreate'])\n ->getMock();\n $contactsRelation->method('where')->willReturnSelf();\n $contactsRelation->method('withTrashed')->willReturnSelf();\n $contactsRelation->method('first')->willReturn(null);\n $contactsRelation->method('updateOrCreate')->willReturn($contact);\n\n $config = $this->createMock(Configuration::class);\n $config->method('contacts')->willReturn($contactsRelation);\n $config->method('accounts')->willReturn($contactsRelation);\n\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $prospectPhotoPathService->method('getOrGeneratePhotoPath')->willReturn('photo.jpg');\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $this->createMock(Client::class),\n $this->createMock(PayloadBuilder::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(CountriesMap::class),\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['handleObjectDeletion'])\n ->getMock();\n\n $service->config = $config;\n $service->profile = $this->createMock(Profile::class);\n\n $team = $this->createMock(Team::class);\n $team->method('getAttribute')->with('id')->willReturn(1);\n $service->team = $team;\n\n $service->method('handleObjectDeletion');\n\n $result = self::invokePrivateMethod('importContact', $service, [$crmData]);\n\n $this->assertNull($result);\n }\n\n public function testImportContactReturnsContactWhenActive(): void\n {\n $crmData = [\n 'Id' => 'SF-CON-ACTIVE',\n 'IsDeleted' => false,\n 'OwnerId' => null,\n 'Name' => 'Active Contact',\n ];\n\n $contact = $this->createMock(Contact::class);\n $contact->method('trashed')->willReturn(false);\n\n $contactsRelation = $this->getMockBuilder(HasMany::class)\n ->disableOriginalConstructor()\n ->addMethods(['where', 'first', 'withTrashed'])\n ->onlyMethods(['updateOrCreate'])\n ->getMock();\n $contactsRelation->method('where')->willReturnSelf();\n $contactsRelation->method('withTrashed')->willReturnSelf();\n $contactsRelation->method('first')->willReturn(null);\n $contactsRelation->method('updateOrCreate')->willReturn($contact);\n\n $config = $this->createMock(Configuration::class);\n $config->method('contacts')->willReturn($contactsRelation);\n $config->method('accounts')->willReturn($contactsRelation);\n\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n $prospectPhotoPathService->method('getOrGeneratePhotoPath')->willReturn('photo.jpg');\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $this->createMock(Client::class),\n $this->createMock(PayloadBuilder::class),\n $this->createMock(Dispatcher::class),\n $this->createMock(CountriesMap::class),\n $prospectPhotoPathService,\n ])\n ->onlyMethods(['handleObjectDeletion'])\n ->getMock();\n\n $service->config = $config;\n $service->profile = $this->createMock(Profile::class);\n\n $team = $this->createMock(Team::class);\n $team->method('getAttribute')->with('id')->willReturn(1);\n $service->team = $team;\n\n $service->method('handleObjectDeletion');\n\n $result = self::invokePrivateMethod('importContact', $service, [$crmData]);\n\n $this->assertSame($contact, $result);\n }\n\n public static function resolveContactAccountProvider(): array\n {\n return [\n 'no AccountId returns null' => [[], null],\n 'AccountId present' => [['AccountId' => 'ACC-001'], 'ACC-001'],\n ];\n }\n\n /**\n * @dataProvider resolveContactAccountProvider\n */\n public function testResolveContactAccountWithNoAccountId(array $crmData, ?string $expectedId): void\n {\n $service = $this->getServiceMock(['syncAccount']);\n\n if ($expectedId === null) {\n $service->expects($this->never())->method('syncAccount');\n $config = $this->createMock(Configuration::class);\n $config->expects($this->never())->method('accounts');\n $service->config = $config;\n\n $result = self::invokePrivateMethod('resolveContactAccount', $service, [$crmData]);\n $this->assertNull($result);\n\n return;\n }\n\n $account = $this->createMock(\\Jiminny\\Models\\Account::class);\n\n $accountsRelation = $this->getMockBuilder(HasMany::class)\n ->disableOriginalConstructor()\n ->addMethods(['where', 'first'])\n ->getMock();\n $accountsRelation->method('where')->with('crm_provider_id', $expectedId)->willReturnSelf();\n $accountsRelation->method('first')->willReturn($account);\n\n $config = $this->createMock(Configuration::class);\n $config->method('accounts')->willReturn($accountsRelation);\n $service->config = $config;\n\n $service->expects($this->never())->method('syncAccount');\n\n $result = self::invokePrivateMethod('resolveContactAccount', $service, [$crmData]);\n $this->assertSame($account, $result);\n }\n\n public function testResolveContactAccountSyncsWhenNotFoundLocally(): void\n {\n $syncedAccount = $this->createMock(\\Jiminny\\Models\\Account::class);\n\n $accountsRelation = $this->getMockBuilder(HasMany::class)\n ->disableOriginalConstructor()\n ->addMethods(['where', 'first'])\n ->getMock();\n $accountsRelation->method('where')->willReturnSelf();\n $accountsRelation->method('first')->willReturn(null);\n\n $config = $this->createMock(Configuration::class);\n $config->method('accounts')->willReturn($accountsRelation);\n\n $service = $this->getServiceMock(['syncAccount']);\n $service->config = $config;\n\n $service->expects($this->once())\n ->method('syncAccount')\n ->with('ACC-MISSING')\n ->willReturn($syncedAccount);\n\n $result = self::invokePrivateMethod('resolveContactAccount', $service, [['AccountId' => 'ACC-MISSING']]);\n\n $this->assertSame($syncedAccount, $result);\n }\n\n public static function resolveContactCountryCodeProvider(): array\n {\n return [\n 'valid MailingCountryCode' => [['MailingCountryCode' => 'GB'], true, null, 'GB'],\n 'invalid MailingCountryCode falls to null' => [['MailingCountryCode' => 'XX'], false, null, null],\n 'no code, uses MailingCountry converted' => [['MailingCountry' => 'Germany'], null, 'DE', 'DE'],\n 'no code, country name null, uses account' => [['MailingCountry' => 'Unknown'], null, null, 'US'],\n 'no code, no country at all' => [[], null, null, null],\n ];\n }\n\n /**\n * @dataProvider resolveContactCountryCodeProvider\n */\n public function testResolveContactCountryCode(\n array $crmData,\n ?bool $countryExists,\n ?string $convertedCode,\n ?string $expected\n ): void {\n $countriesMap = $this->createMock(CountriesMap::class);\n if ($countryExists !== null) {\n $countriesMap->method('countryExists')->willReturn($countryExists);\n }\n\n $service = $this->getMockBuilder(Service::class)\n ->setConstructorArgs([\n $this->createMock(Client::class),\n $this->createMock(PayloadBuilder::class),\n $this->createMock(Dispatcher::class),\n $countriesMap,\n $this->createMock(ProspectPhotoPathService::class),\n ])\n ->onlyMethods(['convertCountryNameToCode'])\n ->getMock();\n\n $service->profile = $this->createMock(Profile::class);\n\n if (isset($crmData['MailingCountry'])) {\n $service->expects($this->once())\n ->method('convertCountryNameToCode')\n ->with($crmData['MailingCountry'])\n ->willReturn($convertedCode);\n } else {\n $service->expects($this->never())->method('convertCountryNameToCode');\n }\n\n $account = null;\n if ($expected === 'US') {\n $account = new \\Jiminny\\Models\\Account();\n $account->setAttribute('country_code', 'US');\n }\n\n $result = self::invokePrivateMethod('resolveContactCountryCode', $service, [$crmData, $account]);\n\n $this->assertSame($expected, $result);\n }\n\n public static function parseContactPhoneProvider(): array\n {\n return [\n 'empty Phone returns empty' => [['Phone' => ''], null, [[], null]],\n 'no Phone key returns empty' => [[], null, [[], null]],\n ];\n }\n\n /**\n * @dataProvider parseContactPhoneProvider\n */\n public function testParseContactPhoneWithEmptyPhone(array $crmData, ?string $countryCode, array $expected): void\n {\n $service = $this->getServiceMock();\n $result = self::invokePrivateMethod('parseContactPhone', $service, [$countryCode, $crmData]);\n $this->assertSame($expected, $result);\n }\n\n public function testParseContactPhoneWithValidNumber(): void\n {\n $service = $this->getServiceMock();\n $crmData = ['Phone' => '+12025551234'];\n\n [$parsedNumber, $ext] = self::invokePrivateMethod('parseContactPhone', $service, [null, $crmData]);\n\n $this->assertArrayHasKey('phone', $parsedNumber);\n $this->assertSame('+12025551234', $parsedNumber['phone']);\n $this->assertNull($ext);\n }\n\n public function testParseContactPhoneWithExtension(): void\n {\n $service = $this->getServiceMock();\n $crmData = ['Phone' => '+12025551234 ext. 42'];\n\n [$parsedNumber, $ext] = self::invokePrivateMethod('parseContactPhone', $service, ['US', $crmData]);\n\n $this->assertArrayHasKey('phone', $parsedNumber);\n $this->assertSame('42', $ext);\n }\n\n public function testParseContactPhoneExtIsTruncatedTo10Chars(): void\n {\n $service = $this->getServiceMock();\n $crmData = ['Phone' => '+12025551234 ext. 12345678901'];\n\n [$parsedNumber, $ext] = self::invokePrivateMethod('parseContactPhone', $service, ['US', $crmData]);\n\n $this->assertNotNull($ext);\n $this->assertLessThanOrEqual(10, strlen($ext));\n }\n\n public function testParseContactPhoneWithCountryCode(): void\n {\n $service = $this->getServiceMock();\n $crmData = ['Phone' => '02075551234'];\n\n [$parsedNumber, $ext] = self::invokePrivateMethod('parseContactPhone', $service, ['GB', $crmData]);\n\n $this->assertArrayHasKey('phone', $parsedNumber);\n $this->assertStringStartsWith('+44', $parsedNumber['phone']);\n $this->assertNull($ext);\n }\n\n public static function parseContactMobileProvider(): array\n {\n return [\n 'empty MobilePhone returns null' => [['MobilePhone' => ''], null, null],\n 'no MobilePhone key returns null' => [[], null, null],\n ];\n }\n\n /**\n * @dataProvider parseContactMobileProvider\n */\n public function testParseContactMobileWithEmptyPhone(array $crmData, ?string $countryCode, ?string $expected): void\n {\n $service = $this->getServiceMock();\n $result = self::invokePrivateMethod('parseContactMobile', $service, [$countryCode, $crmData]);\n $this->assertSame($expected, $result);\n }\n\n public function testParseContactMobileWithValidNumber(): void\n {\n $service = $this->getServiceMock();\n $crmData = ['MobilePhone' => '+12025550199'];\n\n $result = self::invokePrivateMethod('parseContactMobile', $service, [null, $crmData]);\n\n $this->assertNotNull($result);\n $this->assertSame('+12025550199', $result);\n }\n\n public function testParseContactMobileIsTruncatedTo25Chars(): void\n {\n $service = $this->getServiceMock();\n $longNumber = '+1202555019912345678901234';\n $crmData = ['MobilePhone' => $longNumber];\n\n $result = self::invokePrivateMethod('parseContactMobile', $service, [null, $crmData]);\n\n $this->assertNotNull($result);\n $this->assertLessThanOrEqual(25, strlen($result));\n }\n\n public function testParseContactMobileWithCountryCode(): void\n {\n $service = $this->getServiceMock();\n $crmData = ['MobilePhone' => '07911123456'];\n\n $result = self::invokePrivateMethod('parseContactMobile', $service, ['GB', $crmData]);\n\n $this->assertNotNull($result);\n $this->assertStringStartsWith('+44', $result);\n }\n\n public function testImportOpportunitySkipsWhenProfileNotFound(): void\n {\n $crmData = [\n 'Id' => 'SF-NO-USER-2',\n 'Name' => 'Test Opportunity',\n 'OwnerId' => 'owner-not-found',\n // No AccountId - avoid complex account processing\n ];\n\n $client = $this->createMock(Client::class);\n $payloadBuilder = $this->createMock(PayloadBuilder::class);\n $eventDispatcher = $this->createMock(\\Illuminate\\Events\\Dispatcher::class); // ← ADD THIS\n $countriesMap = $this->createMock(CountriesMap::class);\n $prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);\n\n\n $service = new Service(\n $client,\n $payloadBuilder,\n $eventDispatcher,\n $countriesMap,\n $prospectPhotoPathService\n );\n\n $config = $this->createMock(Configuration::class);\n\n // Mock profiles relation returning null (no profile found)\n $profilesRelation = \\Mockery::mock(\\Illuminate\\Database\\Eloquent\\Relations\\HasMany::class);\n $profilesRelation->shouldReceive('where')->with('crm_provider_id', 'owner-not-found')->andReturnSelf();\n $profilesRelation->shouldReceive('first')->andReturn(null);\n\n $config->expects($this->once())\n ->method('profiles')\n ->willReturn($profilesRelation);\n\n $team = $this->createMock(Team::class);\n $team->method('getId')->willReturn(1);\n\n $logger = $this->createMock(\\Psr\\Log\\LoggerInterface::class);\n $logger->expects($this->once())\n ->method('error')\n ->with(\n '[Salesforce] | Skip import, no user_id found',\n ['id' => 'SF-NO-USER-2']\n );\n\n $reflection = new \\ReflectionClass($service);\n\n $configProperty = $reflection->getProperty('config');\n $configProperty->setAccessible(true);\n $configProperty->setValue($service, $config);\n\n $teamProperty = $reflection->getProperty('team');\n $teamProperty->setAccessible(true);\n $teamProperty->setValue($service, $team);\n\n $loggerProperty = $reflection->getProperty('logger');\n $loggerProperty->setAccessible(true);\n $loggerProperty->setValue($service, $logger);\n\n // Initialize profile property\n $profileProperty = $reflection->getProperty('profile');\n $profileProperty->setAccessible(true);\n $profileProperty->setValue($service, null);\n\n $result = self::invokePrivateMethod('importOpportunity', $service, [$crmData]);\n\n $this->assertNull($result);\n }\n}\n\nclass HasManyExtended extends HasMany\n{\n public function where()\n {\n }\n\n public function first()\n {\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.41589096,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.4245346,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.43550533,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.44414893,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.45279256,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.4637633,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.47473404,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.5013298,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.51230055,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.7237367,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"45","depth":4,"bounds":{"left":0.6938165,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.70611703,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"41","depth":4,"bounds":{"left":0.71542555,"top":0.123703115,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"66","depth":4,"bounds":{"left":0.72706115,"top":0.123703115,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7390292,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3282220449727113155
|
-6520523286568608991
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
ServiceTest
Rerun 'PHPUnit: ServiceTest'
Debug 'ServiceTest'
Stop 'ServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Built-in Preview
Chrome
Firefox
Safari
Sync Changes
Hide This Notification
Code changed:
Hide
3
32
81
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Crm\Salesforce;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Facades\Cache;
use Jiminny\Component\Country\CountriesMap;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Enums\CrmObject;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\NoResultsException;
use Jiminny\Integrations\PlaybookResolver;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Playbook;
use Jiminny\Models\Team;
use Jiminny\Models\TeamSettings;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Salesforce\Client;
use Jiminny\Services\Crm\Salesforce\PayloadBuilder;
use Jiminny\Services\Crm\Salesforce\QueryBuilder;
use Jiminny\Services\Crm\Salesforce\QueryHandler;
use Jiminny\Services\Crm\Salesforce\QueryIterator;
use Jiminny\Services\Crm\Salesforce\QueryResults;
use Jiminny\Services\Crm\Salesforce\Service;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;
use Tests\Unit\Traits\TestPrivateMethod;
class ServiceTest extends TestCase
{
use TestPrivateMethod;
public function testFetchAndAssociateRelatedActivity(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$payloadBuilder->method('addCustomLogicFieldsPayload')
->willReturnCallback(function ($activity, $payload) {
return $payload;
});
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods(['fetchRelatedActivity', 'getPlaybook', 'getPlaybookCategory', 'updateRecord'])
->getMock();
$serviceMock->expects($this->once())
->method('fetchRelatedActivity')
->willReturn([
'Id' => 'testId',
'Type' => null,
'OwnerId' => 'testerUser',
'Description' => 'Test description',
]);
$user = $this->createMock(User::class);
$team = $this->createMock(Team::class);
$user->method('getAttribute')->with('team')->willReturn($team);
$playbook = $this->createMock(Playbook::class);
$playbook->method('getActivityField')->willReturn(null);
$playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_EVENT);
$serviceMock->expects($this->once())
->method('getPlaybook')
->with($user)
->willReturn($playbook);
$serviceMock->expects($this->never())
->method('getPlaybookCategory');
$serviceMock->expects($this->never())
->method('updateRecord');
$fieldDataRepository = $this->createMock(FieldDataRepository::class);
$fieldDataRepository->method('getActivityFieldData')->willReturn(collect([]));
app()->instance(FieldDataRepository::class, $fieldDataRepository);
$config = $this->createMock(Configuration::class);
$profilesRelation = $this->getMockBuilder(\Illuminate\Database\Eloquent\Relations\HasMany::class)
->disableOriginalConstructor()
->onlyMethods(['get'])
->addMethods(['where'])
->getMock();
$profilesRelation->method('where')->willReturnSelf();
$profilesRelation->method('get')->willReturn(collect([]));
$config->method('profiles')->willReturn($profilesRelation);
$serviceMock->config = $config;
$serviceMock->profile = null;
$actualStartTime = \Carbon\Carbon::now();
$activity = $this->getMockBuilder(Activity::class)
->disableOriginalConstructor()
->onlyMethods(['update', 'hasProspect'])
->getMock();
$activity->method('update')->willReturn(true);
$activity->method('hasProspect')->willReturn(true);
$activity->type = Activity::TYPE_CONFERENCE;
$activity->provider = Activity::PROVIDER_TWILIO;
$activity->lead_id = 1;
$activity->user_id = 0;
$activity->id_string = 'test-activity-id';
$activity->user = $user;
$activity->actual_start_time = $actualStartTime;
$activity->uuid = 'c53d8320-f556-4cee-a2f8-5f232f454ca4';
app()->bind(PlaybookResolver::class, function () use ($user) {
$playbook = $this->createMock(Playbook::class);
$playbookResolver = $this->createMock(PlaybookResolver::class);
$playbookResolver->expects($this->once())
->method('resolvePlaybookByUser')
->with($user)
->willReturn($playbook);
return $playbookResolver;
});
$data = $serviceMock->fetchAndAssociateRelatedActivity($activity);
$this->assertInstanceOf(Activity::class, $data);
$this->assertEquals(Activity::TYPE_CONFERENCE, $data->getType());
$this->assertEquals($actualStartTime->getTimestamp(), $data->getActualStartTime()->getTimestamp());
}
public function testFetchAndAssociateRelatedActivitySkipsForTaskBasedPlaybook(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods(['fetchRelatedActivity', 'getPlaybook'])
->getMock();
$user = $this->createMock(User::class);
$playbook = $this->createMock(Playbook::class);
$playbook->method('getActivityType')->willReturn(Playbook::ACTIVITY_TYPE_TASK);
$playbook->method('getId')->willReturn(123);
$serviceMock->expects($this->once())
->method('getPlaybook')
->with($user)
->willReturn($playbook);
$serviceMock->expects($this->never())
->method('fetchRelatedActivity');
$activity = $this->getMockBuilder(Activity::class)
->disableOriginalConstructor()
->onlyMethods(['hasProspect', 'getUuid'])
->getMock();
$activity->method('hasProspect')->willReturn(true);
$activity->method('getUuid')->willReturn('c53d8320-f556-4cee-a2f8-5f232f454ca4');
$activity->type = Activity::TYPE_CONFERENCE;
$activity->actual_start_time = \Carbon\Carbon::now();
$activity->user = $user;
$result = $serviceMock->fetchAndAssociateRelatedActivity($activity);
$this->assertNull($result);
}
public function testFetchAndAssociateRelatedActivityReturnsNullForNonConference(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
$serviceMock = new Service(
client: $client,
payloadBuilder: $payloadBuilder,
eventDispatcher: $eventDispatcher,
countriesMap: $countriesMap,
prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class)
);
$activity = $this->getMockBuilder(Activity::class)
->disableOriginalConstructor()
->getMock();
$activity->type = Activity::TYPE_SOFTPHONE;
$result = $serviceMock->fetchAndAssociateRelatedActivity($activity);
$this->assertNull($result);
}
public function testFetchAndAssociateRelatedActivityReturnsNullWhenNoStartTime(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
$serviceMock = new Service(
client: $client,
payloadBuilder: $payloadBuilder,
eventDispatcher: $eventDispatcher,
countriesMap: $countriesMap,
prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class)
);
$activity = $this->getMockBuilder(Activity::class)
->disableOriginalConstructor()
->getMock();
$activity->type = Activity::TYPE_CONFERENCE;
$activity->actual_start_time = null;
$activity->scheduled_start_time = null;
$result = $serviceMock->fetchAndAssociateRelatedActivity($activity);
$this->assertNull($result);
}
public function testFetchAndAssociateRelatedActivityReturnsNullWhenNoProspect(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods(['getPlaybook'])
->getMock();
$serviceMock->expects($this->never())
->method('getPlaybook');
$activity = $this->getMockBuilder(Activity::class)
->disableOriginalConstructor()
->onlyMethods(['hasProspect', 'getUuid'])
->getMock();
$activity->method('hasProspect')->willReturn(false);
$activity->method('getUuid')->willReturn('c53d8320-f556-4cee-a2f8-5f232f454ca4');
$activity->type = Activity::TYPE_CONFERENCE;
$activity->actual_start_time = \Carbon\Carbon::now();
$result = $serviceMock->fetchAndAssociateRelatedActivity($activity);
$this->assertNull($result);
}
public function testMatchExactlyByEmail(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods([])
->getMock();
$profile = new Profile();
$profile->setAttribute('id', bin2hex(random_bytes(8)));
$serviceMock->profile = $profile;
$team = $this->createMock(Team::class);
$serviceMock->team = $team;
$data = $serviceMock->matchExactlyByEmail(bin2hex(random_bytes(8)) . '[EMAIL]');
$this->assertEquals(null, $data);
}
public function testMatchDomainFromEmail(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$queryIterator = $this->createMock(QueryIterator::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
$config = $this->createMock(Configuration::class);
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->any())
->method('search')
->willReturn($queryIterator);
return $handler;
});
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods(['convertCrmData'])
->getMock();
$profile = new Profile();
$profile->account_fields = 'Field1, Field2, Field3';
$serviceMock->profile = $profile;
$serviceMock->expects($this->once())
->method('convertCrmData')
->willReturn(['test']);
$this->app->bind(QueryBuilder::class, function () {
$queryBuilder = $this->createMock(QueryBuilder::class);
$queryBuilder->expects($this->once())
->method('buildMatchByDomainQuery')
->with('[EMAIL]')
->willReturn('FIND {[EMAIL]} IN ALL FIELDS RETURNING Account(Id)');
return $queryBuilder;
});
$team = $this->createMock(Team::class);
$serviceMock->team = $team;
$serviceMock->config = $config;
$data = $serviceMock->matchByDomain('[EMAIL]');
$this->assertEquals(['test'], $data);
}
public function testBuildTaskSearchFields(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
$service = new Service(
client: $client,
payloadBuilder: $payloadBuilder,
eventDispatcher: $eventDispatcher,
countriesMap: $countriesMap,
prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class)
);
$fields = $service->buildTaskSearchFields();
$expectedFields = ['Id', 'WhoId', 'WhatId', 'AccountId'];
$this->assertEquals($expectedFields, $fields);
}
public function testMapCrmObjects(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
$service = new Service(
client: $client,
payloadBuilder: $payloadBuilder,
eventDispatcher: $eventDispatcher,
countriesMap: $countriesMap,
prospectPhotoPathService: $this->createMock(ProspectPhotoPathService::class),
);
$sampleTask = [
'WhoId' => '003sampleWhoId',
'AccountId' => 'sampleAccountId',
'WhatId' => 'sampleWhatId',
];
$activityData = $service->mapCrmObjects($sampleTask);
$expectedActivityData = [
'contact' => '003sampleWhoId',
'account' => 'sampleAccountId',
'opportunity' => 'sampleWhatId',
];
$this->assertEquals($expectedActivityData, $activityData);
}
public function testGetInstalledAppVersion(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
$queryIterator = $this->createMock(QueryIterator::class);
$queryIterator->expects($this->any())
->method('current')->willReturn([
'SubscriberPackageVersion' => [
'MajorVersion' => '1',
'MinorVersion' => '0',
'PatchVersion' => '1',
'BuildNumber' => '0',
],
]);
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->any())
->method('metadata')
->willReturn($queryIterator);
return $handler;
});
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods(array_diff(get_class_methods(Service::class), ['getInstalledAppVersion']))
->getMock();
$version = $serviceMock->getInstalledAppVersion();
$this->assertEquals('1010', $version);
}
public function testSyncProfiles(): void
{
$userToSearch = null;
$team = $this->createMock(Team::class);
$config = $this->createMockedConfiguration();
$config->expects($this->once())
->method('getId')
->willReturn(1);
$salesforceUser = [
'Email' => '[EMAIL]',
'UserPreferencesLightningExperiencePreferred' => true,
'CallCenterId' => '123',
'Id' => '456',
'ProfileId' => '789',
];
app()->bind(QueryBuilder::class, function () use ($userToSearch) {
$queryBuilder = $this->createMock(QueryBuilder::class);
$queryBuilder->expects($this->once())
->method('buildGetUsersQuery')
->with($userToSearch)
->willReturn('SELECT * FROM Users');
return $queryBuilder;
});
$queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults([$salesforceUser], 1, true, null));
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->any())
->method('query')
->with('SELECT * FROM Users')
->willReturn($queryIterator);
return $handler;
});
$user = $this->createMock(User::class);
$user->expects($this->once())
->method('getId')
->willReturn(123);
$this->mockTeamRepository($team, $salesforceUser, $user);
$profileRepository = $this->createMock(ProfileRepository::class);
$profileRepository->expects($this->once())
->method('updateOrCreateProfile')
->with(
$user,
[
'crm_configuration_id' => 1,
'crm_provider_id' => '456',
],
[
'user_id' => 123,
'edition' => Profile::EDITION_LIGHTNING,
'has_external_cti' => true,
'crm_profile_id' => '789',
]
)
->willReturn(new Profile());
$this->app->instance(ProfileRepository::class, $profileRepository);
$serviceMock = $this->getServiceMock();
$serviceMock->team = $team;
$serviceMock->config = $config;
$result = $serviceMock->syncProfiles($userToSearch);
$this->assertNull($result);
}
public function testSyncProfilesEmailIsNull(): void
{
$userToSearch = $this->createMock(User::class);
$salesforceUser = [
'Email' => null,
];
app()->bind(QueryBuilder::class, function () {
$queryBuilder = $this->createMock(QueryBuilder::class);
$queryBuilder->expects($this->once())
->method('buildGetUsersQuery')
->with(null)
->willReturn('SELECT * FROM Users');
return $queryBuilder;
});
$queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults([$salesforceUser], 1, true, null));
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->exactly(2))
->method('query')
->willReturn($queryIterator);
return $handler;
});
$team = $this->createMock(Team::class);
$user = $this->createMock(User::class);
$this->mockTeamRepository($team, $salesforceUser, $user, false);
$config = $this->createMock(Configuration::class);
$serviceMock = $this->getServiceMock();
$serviceMock->team = $team;
$serviceMock->config = $config;
$profile = $serviceMock->syncProfiles(null);
$this->assertNull($profile);
}
public function testSyncProfilesUserToSearchMatchesCurrentUser(): void
{
$userToSearch = $this->createMock(User::class);
$userToSearch->expects($this->once())
->method('getId')
->willReturn(123);
$team = $this->createMock(Team::class);
$config = $this->createMock(Configuration::class);
$config->expects($this->once())
->method('getId')
->willReturn(1);
$salesforceUser = [
'Email' => '[EMAIL]',
'UserPreferencesLightningExperiencePreferred' => true,
'CallCenterId' => '123',
'Id' => '456',
'ProfileId' => '789',
];
app()->bind(QueryBuilder::class, function () use ($userToSearch) {
$queryBuilder = $this->createMock(QueryBuilder::class);
$queryBuilder->expects($this->once())
->method('buildGetUsersQuery')
->with($userToSearch)
->willReturn('SELECT * FROM Users');
return $queryBuilder;
});
$queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults([$salesforceUser], 1, true, null));
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->any())
->method('query')
->with('SELECT * FROM Users')
->willReturn($queryIterator);
return $handler;
});
$user = $this->createMock(User::class);
$user->expects($this->exactly(2))
->method('getId')
->willReturn(123);
$this->mockTeamRepository($team, $salesforceUser, $user);
$profileRepository = $this->createMock(ProfileRepository::class);
$profileRepository->expects($this->once())
->method('updateOrCreateProfile')
->with(
$user,
[
'crm_configuration_id' => 1,
'crm_provider_id' => '456',
],
[
'user_id' => 123,
'edition' => Profile::EDITION_LIGHTNING,
'has_external_cti' => true,
'crm_profile_id' => '789',
]
)
->willReturn(new Profile());
$this->app->instance(ProfileRepository::class, $profileRepository);
$serviceMock = $this->getServiceMock();
$serviceMock->team = $team;
$serviceMock->config = $config;
$profile = $serviceMock->syncProfiles($userToSearch);
$this->assertInstanceOf(Profile::class, $profile);
}
public function testSyncProfilesWithCustomValidation(): void
{
$userToSearch = null;
$team = $this->createMock(Team::class);
$config = $this->createMockedConfiguration();
$config->expects($this->atLeastOnce()) // Changed from once() to atLeastOnce()
->method('getId')
->willReturn(1);
$salesforceUser = [
'Email' => '[EMAIL]',
'UserPreferencesLightningExperiencePreferred' => true,
'CallCenterId' => '123',
'Id' => '456',
'ProfileId' => '789',
'CustomField' => 'CustomValue',
];
$customRules = [
['field' => 'CustomField', 'value' => 'CustomValue'],
];
$this->mockQueryBuilderAndHandler($userToSearch, [$salesforceUser]);
$user = $this->createMock(User::class);
$user->expects($this->once())
->method('getId')
->willReturn(123);
$this->mockTeamRepository($team, $salesforceUser, $user, true, $customRules);
$this->mockProfileRepository($user);
$serviceMock = $this->getServiceMock();
$serviceMock->team = $team;
$serviceMock->config = $config;
$profile = $serviceMock->syncProfiles($userToSearch);
$this->assertNull($profile);
}
public function testSyncProfilesWithCustomValidationFailing(): void
{
$userToSearch = null;
$team = $this->createMock(Team::class);
$config = $this->createMockedConfiguration();
$salesforceUser = [
'Email' => '[EMAIL]',
'UserPreferencesLightningExperiencePreferred' => true,
'CallCenterId' => '123',
'Id' => '456',
'ProfileId' => '789',
'CustomField' => 'WrongValue',
];
$customRules = [
['field' => 'CustomField', 'value' => 'CustomValue'],
];
$this->mockQueryBuilderAndHandler($userToSearch, [$salesforceUser]);
$teamRepository = $this->getMockForAbstractClass(TeamRepository::class, [], '', false, true, true, ['findActiveTeamMemberByEmail', 'getTeamSetting']);
$teamSettings = $this->createMock(TeamSettings::class);
$teamSettings->method('getValueType')
->willReturn('array');
$teamSettings->method('getValue')
->willReturn(json_encode($customRules));
$teamRepository->expects($this->once())
->method('getTeamSetting')
->with($team, 'custom_profile_validation')
->willReturn($teamSettings);
app()->bind(TeamRepository::class, function () use ($teamRepository) {
return $teamRepository;
});
$profileRepository = $this->createMock(ProfileRepository::class);
$profileRepository->expects($this->never())
->method('updateOrCreateProfile');
$this->app->instance(ProfileRepository::class, $profileRepository);
$serviceMock = $this->getServiceMock();
$serviceMock->team = $team;
$serviceMock->config = $config;
$result = $serviceMock->syncProfiles($userToSearch);
$this->assertNull($result);
}
public function testGetContactRolesFromCrm(): void
{
$contactRoles = [
[
'Id' => '1',
'ContactId' => 'Contact1',
'OpportunityId' => 'Opportunity1',
'Opportunity' => ['OwnerId' => 'Owner1'],
'IsPrimary' => true,
'Role' => 'Decision Maker',
],
];
$expectedResponse = [
[
'id' => '1',
'contactId' => 'Contact1',
'opportunityId' => 'Opportunity1',
'ownerId' => 'Owner1',
'isPrimary' => true,
'role' => 'Decision Maker',
],
];
$this->bindQueryIterator($contactRoles);
$serviceMock = $this->getServiceMock();
$data = $serviceMock->getContactRolesFromCrm(now()->subDay());
$this->assertEquals($expectedResponse, $data);
}
public function testGetContactRolesFromCrmNoResult(): void
{
$this->bindQueryIterator([]);
$serviceMock = $this->getServiceMock();
$data = $serviceMock->getContactRolesFromCrm(now()->subDay());
$this->assertEquals([], $data);
}
public function testSyncContactRoles(): void
{
$contactRoles = [
[
'id' => '1',
'contactId' => 'Contact1',
'opportunityId' => 'Opportunity1',
'ownerId' => 'Owner1',
'isPrimary' => true,
'role' => 'Decision Maker',
],
];
app()->bind(ContactRoleRepository::class, function () {
$contactRoleRepository = $this->createMock(ContactRoleRepository::class);
$contactRoleRepository->expects($this->once())
->method('saveContactRoles');
return $contactRoleRepository;
});
$serviceMock = $this->getServiceMock([
'getContactRolesFromCrm',
'syncRemotelyDeletedContactRoles',
'syncContact',
'syncOpportunity',
]);
$config = $this->createMock(Configuration::class);
$hasMany = $this->createMock(HasManyExtended::class);
$hasMany->expects($this->exactly(2))
->method('where')
->willReturn($hasMany);
$hasMany->expects($this->exactly(2))
->method('first')
->willReturn(
$this->createMock(Contact::class),
$this->createMock(Opportunity::class)
);
$config->expects($this->once())
->method('contacts')
->willReturn($hasMany);
$config->expects($this->once())
->method('opportunities')
->willReturn($hasMany);
$serviceMock->config = $config;
$serviceMock->expects($this->once())
->method('getContactRolesFromCrm')
->willReturn($contactRoles);
$serviceMock->expects($this->once())
->method('syncRemotelyDeletedContactRoles');
$data = $serviceMock->syncContactRoles(now()->subDay());
$this->assertEquals(1, $data);
}
public function testSyncRemotelyDeletedContactRoles(): void
{
$contactRoles = [
[
'id' => '1',
'crm_provider_id' => '1',
],
];
app()->bind(QueryHandler::class, function () use ($contactRoles) {
$queryResults = new QueryResults($contactRoles, 1, true, null);
$handler = $this->createMock(QueryHandler::class);
$handler->method('queryDeleted')
->willReturn($queryResults);
return $handler;
});
app()->bind(ContactRoleRepository::class, function () {
$contactRoleRepository = $this->createMock(ContactRoleRepository::class);
$contactRoleRepository->expects($this->once())
->method('deleteContactRoles');
return $contactRoleRepository;
});
$serviceMock = $this->getServiceMock();
$serviceMock->team = $this->createMock(Team::class);
$data = $this->invokePrivateMethod('syncRemotelyDeletedContactRoles', $serviceMock, []);
$this->assertTrue($data);
}
private function bindQueryIterator(array $queryResult): void
{
/** @var Client $client */
$client = $this->createMock(Client::class);
$queryIterator = new QueryIterator(
$client,
new QueryResults($queryResult, 1, true, null)
);
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->any())
->method('query')
->willReturn($queryIterator);
return $handler;
});
}
public static function getOpportunitySortOrderDataProvider(): array
{
return [
'all open recently updated' => [Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED, ['LastModifiedDate DESC', true]],
'all open recently created' => [Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED, ['CreatedDate DESC', true]],
'all open oldest created' => [Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED, ['CreatedDate ASC', true]],
'all recently updated' => [Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED, ['LastModifiedDate DESC', false]],
'default' => ['unknown', ['LastModifiedDate DESC', true]],
];
}
private function createMockedConfiguration(): Configuration
{
$config = $this->createMock(Configuration::class);
$profilesRelation = $this->getMockBuilder(\Illuminate\Database\Eloquent\Relations\HasMany::class)
->disableOriginalConstructor()
->onlyMethods(['get'])
->addMethods(['where', 'first'])
->getMock();
$profilesRelation->method('where')->willReturnSelf();
$profilesRelation->method('get')->willReturn(collect([]));
$profilesRelation->method('first')->willReturn(null);
$config->method('profiles')->willReturn($profilesRelation);
return $config;
}
private function getServiceMock(array $onlyMethods = []): MockObject&Service
{
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$this->createMock(Client::class),
$this->createMock(PayloadBuilder::class),
$this->createMock(Dispatcher::class),
$this->createMock(CountriesMap::class),
$this->createMock(ProspectPhotoPathService::class),
])
->onlyMethods($onlyMethods)
->getMock();
$serviceMock->profile = $this->createMock(Profile::class);
return $serviceMock;
}
private function mockQueryBuilderAndHandler($userToSearch, $salesforceUsers): void
{
app()->bind(QueryBuilder::class, function () use ($userToSearch) {
$queryBuilder = $this->createMock(QueryBuilder::class);
$queryBuilder->expects($this->once())
->method('buildGetUsersQuery')
->with($userToSearch)
->willReturn('SELECT * FROM Users');
return $queryBuilder;
});
$queryIterator = new QueryIterator($this->createMock(Client::class), new QueryResults($salesforceUsers, count($salesforceUsers), true, null));
app()->bind(QueryHandler::class, function () use ($queryIterator) {
$handler = $this->createMock(QueryHandler::class);
$handler->expects($this->any())
->method('query')
->willReturn($queryIterator);
return $handler;
});
}
private function mockTeamRepository(
Team $team,
array $salesforceUser,
?User $user = null,
bool $userSearch = true,
array $customRules = []
): void {
$teamRepository = $this->getMockForAbstractClass(TeamRepository::class, [], '', false, true, true, ['findActiveTeamMemberByEmail', 'getTeamSetting']);
if ($userSearch) {
$teamRepository->expects($this->once())
->method('findActiveTeamMemberByEmail')
->with($team, $salesforceUser['Email'])
->willReturn($user);
}
$teamSettings = $this->createMock(TeamSettings::class);
$teamSettings->method('getValueType')
->willReturn('array');
$teamSettings->method('getValue')
->willReturn(json_encode($customRules));
$teamRepository->expects($this->once())
->method('getTeamSetting')
->with($team, 'custom_profile_validation')
->willReturn($teamSettings);
app()->bind(TeamRepository::class, function () use ($teamRepository) {
return $teamRepository;
});
}
private function mockProfileRepository(User $user): void
{
$profileRepository = $this->createMock(ProfileRepository::class);
$profileRepository->expects($this->once())
->method('updateOrCreateProfile')
->with(
$user,
[
'crm_configuration_id' => 1,
'crm_provider_id' => '456',
],
[
'user_id' => 123,
'edition' => Profile::EDITION_LIGHTNING,
'has_external_cti' => true,
'crm_profile_id' => '789',
]
)
->willReturn(new Profile());
$this->app->instance(ProfileRepository::class, $profileRepository);
}
public function testBuildEnhancedNoteDecodesHtmlEntities(): void
{
$service = $this->getServiceMock(['createRecord']);
$profile = new Profile();
$profile->setAttribute('crm_provider_id', 'owner-123');
$service->profile = $profile;
$service->expects($this->exactly(2))
->method('createRecord')
->willReturnOnConsecutiveCalls('note-id-123', 'link-id-456');
$bodyWithEntities = 'Welch's current challenges and Facebook's Club';
$result = $this->invokePrivateMethod('buildEnhancedNote', $service, [
'Test Title',
$bodyWithEntities,
'object-id-789',
]);
$this->assertEquals('note-id-123', $result);
}
public function testBuildEnhancedNoteSanitizesWithoutQuotes(): void
{
$service = $this->getServiceMock(['createRecord']);
$profile = new Profile();
$profile->setAttribute('crm_provider_id', 'owner-456');
$service->profile = $profile;
$service->expects($this->exactly(2))
->method('createRecord')
->willReturnCallback(function ($type, $data) {
if ($type === 'ContentNote') {
$decoded = base64_decode($data['Content']);
$this->assertStringContainsString("Welch's", $decoded);
$this->assertStringNotContainsString(''', $decoded);
$this->assertStringNotContainsString('&#039;', $decoded);
$this->assertStringContainsString('<script>', $decoded);
return 'note-id-456';
}
return 'link-id-789';
});
$bodyWithMixedContent = "Welch's and <script>alert('xss')</script>";
$result = $this->invokePrivateMethod('buildEnhancedNote', $service, [
'Test Title',
$bodyWithMixedContent,
'object-id-123',
]);
$this->assertEquals('note-id-456', $result);
}
public function testBuildEnhancedNoteConvertsLineBreaks(): void
{
$service = $this->getServiceMock(['createRecord']);
$profile = new Profile();
$profile->setAttribute('crm_provider_id', 'owner-789');
$service->profile = $profile;
$service->expects($this->exactly(2))
->method('createRecord')
->willReturnCallback(function ($type, $data) {
if ($type === 'ContentNote') {
$decoded = base64_decode($data['Content']);
$this->assertStringContainsString('<br>', $decoded);
$this->assertStringNotContainsString('<br />', $decoded);
return 'note-id-789';
}
return 'link-id-012';
});
$bodyWithLineBreaks = "Line 1\nLine 2\nLine 3";
$result = $this->invokePrivateMethod('buildEnhancedNote', $service, [
'Test Title',
$bodyWithLineBreaks,
'object-id-456',
]);
$this->assertEquals('note-id-789', $result);
}
public function testBuildEnhancedNoteHandlesComplexScenario(): void
{
$service = $this->getServiceMock(['createRecord']);
$profile = new Profile();
$profile->setAttribute('crm_provider_id', 'owner-complex');
$service->profile = $profile;
$service->expects($this->exactly(2))
->method('createRecord')
->willReturnCallback(function ($type, $data) {
if ($type === 'ContentNote') {
$decoded = base64_decode($data['Content']);
$this->assertStringContainsString("Welch's", $decoded);
$this->assertStringContainsString("Facebook's Club", $decoded);
$this->assertStringContainsString("Arctics'", $decoded);
$this->assertStringNotContainsString(''', $decoded);
$this->assertStringNotContainsString('&#039;', $decoded);
$this->assertStringContainsString('<br>', $decoded);
$this->assertStringContainsString('<', $decoded);
$this->assertStringContainsString('>', $decoded);
return 'note-complex';
}
return 'link-complex';
});
$complexBody = "Summary:\n---------\nThe call focused on understanding Welch's current challenges and exploring how Arctics' Revenue Growth Management solutions could support their strategic goals.\n\n• John SMith discussed his role as a category advisor for Google and Facebook's Club, emphasizing the importance of market research and advising on product assortment.\n• Madona introduced Arctics' Virtual Shoppers AI, which simulates consumer <behavior> to optimize pricing and promotional strategies.";
$result = $this->invokePrivateMethod('buildEnhancedNote', $service, [
'Jiminny Transcription Summary',
$complexBody,
'task-id-001',
]);
$this->assertEquals('note-complex', $result);
}
public function testSyncRemotelyDeletedObjectsWithErrorHandlingSuccess(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$prospectPhotoPathService,
])
->onlyMethods(['syncRemotelyDeletedObjects'])
->getMock();
// Mock team
$team = $this->createMock(Team::class);
$team->method('getUuid')->willReturn('team-uuid-123');
$serviceMock->team = $team;
// Expect syncRemotelyDeletedObjects to be called once and succeed
$serviceMock->expects($this->once())
->method('syncRemotelyDeletedObjects')
->with(\Jiminny\Enums\CrmObject::ACCOUNT);
// Call the protected method using reflection
$reflection = new \ReflectionClass($serviceMock);
$method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');
$method->setAccessible(true);
// Should not throw any exceptions
$method->invoke($serviceMock, \Jiminny\Enums\CrmObject::ACCOUNT);
$this->assertTrue(true); // Test completed successfully
}
public function testSyncRemotelyDeletedObjectsWithErrorHandlingFailure(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$prospectPhotoPathService,
])
->onlyMethods(['syncRemotelyDeletedObjects'])
->getMock();
// Mock team
$team = $this->createMock(Team::class);
$team->method('getUuid')->willReturn('team-uuid-456');
$serviceMock->team = $team;
// Mock logger to verify warning is logged
$logger = $this->createMock(\Psr\Log\LoggerInterface::class);
// Use reflection to set the protected logger property
$reflection = new \ReflectionClass($serviceMock);
$loggerProperty = $reflection->getProperty('logger');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($serviceMock, $logger);
$exception = new \Exception('Sync failed due to API error');
// Expect syncRemotelyDeletedObjects to throw an exception
$serviceMock->expects($this->once())
->method('syncRemotelyDeletedObjects')
->with(\Jiminny\Enums\CrmObject::CONTACT)
->willThrowException($exception);
// Expect warning to be logged with correct message and parameters
$logger->expects($this->once())
->method('warning')
->with(
'[Salesforce] Remotely deleted objects sync failed',
[
'objectType' => 'contact',
'teamId' => 'team-uuid-456',
'reason' => 'Sync failed due to API error',
]
);
// Call the protected method using reflection
$reflection = new \ReflectionClass($serviceMock);
$method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');
$method->setAccessible(true);
// Should not re-throw the exception, just log it
$method->invoke($serviceMock, \Jiminny\Enums\CrmObject::CONTACT);
$this->assertTrue(true); // Test completed successfully
}
public function testSyncRemotelyDeletedObjectsWithErrorHandlingWithLogParams(): void
{
$client = $this->createMock(Client::class);
$payloadBuilder = $this->createMock(PayloadBuilder::class);
$countriesMap = $this->createMock(CountriesMap::class);
$prospectPhotoPathService = $this->createMock(ProspectPhotoPathService::class);
$eventDispatcher = $this->createMock(Dispatcher::class);
/** @var Service&MockObject $serviceMock */
$serviceMock = $this->getMockBuilder(Service::class)
->setConstructorArgs([
$client,
$payloadBuilder,
$eventDispatcher,
$countriesMap,
$prospectPhotoPathService,
])
->onlyMethods(['syncRemotelyDeletedObjects'])
->getMock();
// Mock team
$team = $this->createMock(Team::class);
$team->method('getUuid')->willReturn('team-uuid-789');
$serviceMock->team = $team;
// Mock logger to verify warning is logged
$logger = $this->createMock(\Psr\Log\LoggerInterface::class);
// Use reflection to set the protected logger property
$loggerReflection = new \ReflectionClass($serviceMock);
$loggerProperty = $loggerReflection->getProperty('logger');
$loggerProperty->setAccessible(true);
$loggerProperty->setValue($serviceMock, $logger);
$exception = new \Exception('Network timeout');
// Expect syncRemotelyDeletedObjects to throw an exception
$serviceMock->expects($this->once())
->method('syncRemotelyDeletedObjects')
->with(\Jiminny\Enums\CrmObject::OPPORTUNITY)
->willThrowException($exception);
// Additional log parameters
$logParams = [
'syncType' => 'full',
'batchSize' => 100,
];
// Expect warning to be logged with merged parameters
$logger->expects($this->once())
->method('warning')
->with(
'[Salesforce] Remotely deleted objects sync failed',
[
'objectType' => 'opportunity',
'teamId' => 'team-uuid-789',
'reason' => 'Network timeout',
'syncType' => 'full',
'batchSize' => 100,
]
);
// Call the protected method using reflection
$reflection = new \ReflectionClass($serviceMock);
$method = $reflection->getMethod('syncRemotelyDeletedObjectsWithErrorHandling');
...
|
85733
|
NULL
|
NULL
|
NULL
|
|
85734
|
2938
|
6
|
2026-05-28T13:03:59.948949+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973439948_m1.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.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
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
iTerm2• • 0ShellEditVie Project: faVsco.js, menu
iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKERO ₴1DEV (docker)₴82-zshN3screenpipe"-zshapp/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php348+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976+++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec-it docker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|85100% <78• Thu 28 May 16:03:59L881ec2-user@ip-10-30-129-...ec2-user@ip-10-30-140-...₴7...
|
85732
|
NULL
|
NULL
|
NULL
|
|
85733
|
2939
|
6
|
2026-05-28T13:03:58.245665+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973438245_m2.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rapstomViewCoocFV faVsco.|s ~#12121 on JY-20963-fx rapstomViewCoocFV faVsco.|s ~#12121 on JY-20963-fx-proidet• HttpIntegrationslinteractionsJobsa MailModelsanoureatonsvoseivers(C SocialAccountObserverTest.gUserObserverTest.phpUserRoleObserverTest.phoPolcies>EACtVIYDa ActivityProviderslealendanTeonterenco•Rullmeen& CloseleooneMi HibsootD IntegrationApplllistenarsPipedrive(D Salesforce• (b Field:D OpportunityMatcherOpportunitySyncStratel ProsoectSearchStratecim sancettaisClientTest.phoC DecorateActivityTest.oC DeleteObiectsTrait TestFieldDefinitionsTest.oh© GetActivitvFieldNameTPavloadBuilder [EMAIL]© QuervHandlerTest.oha©QuerviteratorTest.ohr©QuervResuitsTests.ohC Semceltestoo@SwncRatchRedicSenics@ RaseSeniiceTest ohoWindowwnapers.ohyTeom.one©ActivityControlier.phpA SF fiminny@localhost)A console (STAGING)De00 Tx.AutovBY u.id, v.enail, u.name, u.softphone_number* fron teans where id = 1;# console fiaum045 A1 A41 У 66 4class servicerest excends testcasepublic function testFetchAndAssociateRelatedActivityO: voidSatient & Sthis->createlockld onenetientchssSoav loadBusi den & SthisoscrenteVockld cno neteiosePayloadBust den. sclass)SoavloadBusi dens>nethodid constn->willReturnCallback(function (Sactivity, Spayload) (...*)Scountniestan=SthiiscscreateVockltono/** @var Service&MockObnect SserviceMock */SserviceMock = Sthis->getMockBuilder className: Service::class)->setConstructorArgs(CalkontSoayLoadbuzloersevencurspaccherScountriesMapSthis->createMock( originalClass>oncynechodstecchketacecacc.v.cy→>аскносkонwcthoekoseyocerReiaho>oneo>mhodl"fetchRelatedActävity")-sweeerumnia"descriintsion' "Test descriattion"Steam = Sthis->createMock( orig"aetattoibute"oythltearoum"tean"oyil Return SteamplavhonkecllaseSnlavhookoSnethodlTon.ofedtradend v Accept Fle x- 5 X-Papaи fнo p3elnninhilsSplaybook->method( constraint: "getActivityType')->willReturn( value: Playbook::ACTIVITY TYPE EVENT)710711=712713fron text relays where created at > 2926-05-91.fron actsivitsles where user_id TN (7169, 18688) and created at > 2926-85-22* orden by id desci* fron users where team_id = 1 and id IN (18688, 13934, 7160);* fron activities where user_id = 7160 order by id desc Linit 10;*foon teane where ¿d = 1117:foon setkusty coanch filitane nhans actfusty canoch SA TM CARRR) RRO92TO0У L7Thu 28 May 16:03:57eeendhles orchwerwicee+0.testsruexec dovkers/crm/3 bestore 5e/vicer/stmphpy - iterwenest/brseContact' 2561PHPUnit 11.5.55 by Sebastian Bergmann and contributors.conteouratciont no.e/tminayohounsx"11/ 11 (100%Tine: 00:10.908, Memory: 66.00 MBAl 11 new and existing testParseContact* tests pass. The deprecations are pre-existing PHPUnit warningsLet ma aiso ar'cklv nin tha mll SarufcaTast to contmm nothico ie bzokna.• decter exe sdrcker /era/3 bestore Servioeeeitetnay 55 php a5vendor/bin/phpun:PHPUnzt 11.5.55 by Sebastzan Beramann and contributors.Confiquration:/home/siminny/phpunit.xmD.-DODOD..•••0000.DD.......••. 65 / 73 ( 894TIeA: 60:21 704 MamArиs 04160 MpA.7s tests pass. Herc's a summary of what was added.oo eoesThe PR already covered empty/null early-return paths. The missing coverage was for the happy paths - added 7newssanrsctontact phos•testParseContactPhoneWithValidNumber - E.164 number parses correctly, ext is nul?+eParsccontad Pnonewnextens ton -Tmoer wihler dcontoty ertactne exentotactpareacontsctPhonaFyttetruncatettotochare=sytwttolonosthm.th/chhre.abtetnindstodllGtru1taetbareacontnetPhondthrount.rtodesloclkoim.tthto.totowmeine.cotntrv.conparseContactMobile•testParsetontac Nonathaidduher- E6 numaertetumed netActPsreAtontsctwahsntehrunestMhT/20Ghhre-Mimawtlwawthan2f.thare aote thinastodltroledtACtOsrcAGont.CMAKSlWWAthGAuAtruCAA.-TAAITIKmAwAroALAhtArmcimaAMlintrv.oh1tile +80 2Accept allAsk anything (XOL)" PodnAdhotvwodturlhhmeewhi.es*4 space...
|
NULL
|
-2503543311014622480
|
NULL
|
click
|
ocr
|
NULL
|
rapstomViewCoocFV faVsco.|s ~#12121 on JY-20963-fx rapstomViewCoocFV faVsco.|s ~#12121 on JY-20963-fx-proidet• HttpIntegrationslinteractionsJobsa MailModelsanoureatonsvoseivers(C SocialAccountObserverTest.gUserObserverTest.phpUserRoleObserverTest.phoPolcies>EACtVIYDa ActivityProviderslealendanTeonterenco•Rullmeen& CloseleooneMi HibsootD IntegrationApplllistenarsPipedrive(D Salesforce• (b Field:D OpportunityMatcherOpportunitySyncStratel ProsoectSearchStratecim sancettaisClientTest.phoC DecorateActivityTest.oC DeleteObiectsTrait TestFieldDefinitionsTest.oh© GetActivitvFieldNameTPavloadBuilder [EMAIL]© QuervHandlerTest.oha©QuerviteratorTest.ohr©QuervResuitsTests.ohC Semceltestoo@SwncRatchRedicSenics@ RaseSeniiceTest ohoWindowwnapers.ohyTeom.one©ActivityControlier.phpA SF fiminny@localhost)A console (STAGING)De00 Tx.AutovBY u.id, v.enail, u.name, u.softphone_number* fron teans where id = 1;# console fiaum045 A1 A41 У 66 4class servicerest excends testcasepublic function testFetchAndAssociateRelatedActivityO: voidSatient & Sthis->createlockld onenetientchssSoav loadBusi den & SthisoscrenteVockld cno neteiosePayloadBust den. sclass)SoavloadBusi dens>nethodid constn->willReturnCallback(function (Sactivity, Spayload) (...*)Scountniestan=SthiiscscreateVockltono/** @var Service&MockObnect SserviceMock */SserviceMock = Sthis->getMockBuilder className: Service::class)->setConstructorArgs(CalkontSoayLoadbuzloersevencurspaccherScountriesMapSthis->createMock( originalClass>oncynechodstecchketacecacc.v.cy→>аскносkонwcthoekoseyocerReiaho>oneo>mhodl"fetchRelatedActävity")-sweeerumnia"descriintsion' "Test descriattion"Steam = Sthis->createMock( orig"aetattoibute"oythltearoum"tean"oyil Return SteamplavhonkecllaseSnlavhookoSnethodlTon.ofedtradend v Accept Fle x- 5 X-Papaи fнo p3elnninhilsSplaybook->method( constraint: "getActivityType')->willReturn( value: Playbook::ACTIVITY TYPE EVENT)710711=712713fron text relays where created at > 2926-05-91.fron actsivitsles where user_id TN (7169, 18688) and created at > 2926-85-22* orden by id desci* fron users where team_id = 1 and id IN (18688, 13934, 7160);* fron activities where user_id = 7160 order by id desc Linit 10;*foon teane where ¿d = 1117:foon setkusty coanch filitane nhans actfusty canoch SA TM CARRR) RRO92TO0У L7Thu 28 May 16:03:57eeendhles orchwerwicee+0.testsruexec dovkers/crm/3 bestore 5e/vicer/stmphpy - iterwenest/brseContact' 2561PHPUnit 11.5.55 by Sebastian Bergmann and contributors.conteouratciont no.e/tminayohounsx"11/ 11 (100%Tine: 00:10.908, Memory: 66.00 MBAl 11 new and existing testParseContact* tests pass. The deprecations are pre-existing PHPUnit warningsLet ma aiso ar'cklv nin tha mll SarufcaTast to contmm nothico ie bzokna.• decter exe sdrcker /era/3 bestore Servioeeeitetnay 55 php a5vendor/bin/phpun:PHPUnzt 11.5.55 by Sebastzan Beramann and contributors.Confiquration:/home/siminny/phpunit.xmD.-DODOD..•••0000.DD.......••. 65 / 73 ( 894TIeA: 60:21 704 MamArиs 04160 MpA.7s tests pass. Herc's a summary of what was added.oo eoesThe PR already covered empty/null early-return paths. The missing coverage was for the happy paths - added 7newssanrsctontact phos•testParseContactPhoneWithValidNumber - E.164 number parses correctly, ext is nul?+eParsccontad Pnonewnextens ton -Tmoer wihler dcontoty ertactne exentotactpareacontsctPhonaFyttetruncatettotochare=sytwttolonosthm.th/chhre.abtetnindstodllGtru1taetbareacontnetPhondthrount.rtodesloclkoim.tthto.totowmeine.cotntrv.conparseContactMobile•testParsetontac Nonathaidduher- E6 numaertetumed netActPsreAtontsctwahsntehrunestMhT/20Ghhre-Mimawtlwawthan2f.thare aote thinastodltroledtACtOsrcAGont.CMAKSlWWAthGAuAtruCAA.-TAAITIKmAwAroALAhtArmcimaAMlintrv.oh1tile +80 2Accept allAsk anything (XOL)" PodnAdhotvwodturlhhmeewhi.es*4 space...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85732
|
2938
|
5
|
2026-05-28T13:03:58.349211+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973438349_m1.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2• • 0ShellEditViewSessionScripts|ProfilesW iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKERO ₴1DEV (docker)₴82-zshN3screenpipe"O ₴4-zshapp/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php348+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec-it docker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|85ec2-user@ip-10-30-129-...100% <78 • Thu 28 May 16:03:581₴1ec2-user@ip-10-30-140-...₴7...
|
NULL
|
7922818983694723678
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2• • 0ShellEditViewSessionScripts|ProfilesW iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKERO ₴1DEV (docker)₴82-zshN3screenpipe"O ₴4-zshapp/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php348+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec-it docker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|85ec2-user@ip-10-30-129-...100% <78 • Thu 28 May 16:03:581₴1ec2-user@ip-10-30-140-...₴7...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85731
|
2938
|
4
|
2026-05-28T13:03:45.735865+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973425735_m1.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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":"TextRelayServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-5714658179048184897
|
-8634297297851446848
|
idle
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKER-₴81DEV (docker)₴82-zshN3screenpipe"O &4-zshapp/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php348+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec-it docker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|85100% <78• Thu 28 May 16:03:45L881ec2-user@ip-10-30-129-...ec2-user@ip-10-30-140-...₴7...
|
85729
|
NULL
|
NULL
|
NULL
|
|
85730
|
2939
|
5
|
2026-05-28T13:03:44.368156+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973424368_m2.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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.8374335,"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}]...
|
8569994499127135030
|
-3986301007428073024
|
idle
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
rapstomViewCoocFV faVsco.|s ~#12121 on JY-20963-fx-• HttpIntegrationslinteractionsJobsa MailModelsanoureatonsvoseivers(C SocialAccountObserverTest.rUserObserverTest.phpUserRoleObserverTest.phoPolcies>EACtVIYla ActivityProvider:lealendanTeonterenco•RullmeencloseleooneMi HibsootlllistenarsPipedrive(D Salesforce• (b Field:D OpportunityMatcherOpportunitySyncStratel ProsoectSearchStratecim sancettaisClientTest.phoC DecorateActivityTest.oC DeleteObiectsTrait TestAGNAnafniioast eston© GetActivitvFieldNameTPavloadBuilder [EMAIL]© QuervHandlerTest.oha©QuerviteratorTest.ohr©QuervResuitsTests.ohC Semceltestoo@SwncRatchRedicSenics@ RaseSeniiceTest oho38 %9Windowwnapers.ohyTeom.one©ActivityControlier.phpuCicheoneA SF fiminny@localhost)console (PROD) XA console (STAGING)De000T Autowreated at > DATE SUB(NOWO), INTERVAL 38 DAYBY u.id, u.email,u.name u.sotcphone nunbenBY sns_count DESC;7827es* fron teans where id = 1;710711=712713NCAT(u.id, CASE WHEN U.id = t.ouner_id THEN ' (ouner) ' ELSE "* END) AS user_id,# console fiaum045 A1 A41 У 66 4Curssirvcaloslexcondsesstpublic function testFetchAndAssociateRelatedActivityO: voidSatient & Sthis->createlockld onenetientchssSoav loadBusi den & SthisoscrenteVockld cno neteiosePayloadBust den. sclassh.SoavloadBusi dens>nethodid constn->willReturnCallback(function (Sactivity, Spayload) (...*)Scountcestann SthisoscreateVockltonol/** @var Service&MockObnect SserviceMock */SserviceMock = Sthis->getMockBuilder className: Service::class)->setConstructorArgs(CalkontSoayLoadbuzloersevencurspaccherScountriesMapSthis->createMock( originalClassh>oncynechodstecchketacecacc.v.cy→>аскносkонwcthoekoseyocerReiaho>oneo->mnadl ciTerchie fred.cewiy'-sweeerumnia"descriintsion' "Test descriattion"Steam = Sthis->createMock( originTean::class):"aetattoibute"oythltearoum"tean"oyil Return Steamplavhankes cllasshSnlavhonkesnethodlTon.offedtradend v Accept Fle x- 5 X-Papaи fнo p3elninhilsSplaybook->method( constraint: "getActivityType')->willReturn( value: Playbook::ACTIVITY TYPE EVENT) =acrvimes a oN to.aciaviry1d = a.10vider1d = 'hs actavitytypel* FROM activity_messages:fron text rellavs where created.ar > 7826-85-8* fron actsivities where user id TN (7168, 18688) and created at > *2826-85-22' order by sid descl* fron users where team id = 1 and id TN (18688, 13934. 7169)=* fron activities where user_id = 7160 order by id desc Linit 10;* fron accounts where team_id = 1 and nane = 'Columns" :*Thor tasne whendr14* fron actávity_searches where user_id = 31854;*Eoon sctkusty coanch Eilitane nhans actkusty cancch fa TM CRRRR2 RRO02TO0У L7Thu 28 May 16:03:44eeendhlesorceserwicee+0.testsruexec dovkers/crm/3 bestore 5e/vicer/stmphpy - iterwenest/brseContact' 2561PHPUnit 11.5.55 by Sebastian Bergmann and contributors.contelouratciontnore/tinnyl ohounx"11/ 11 (100%Tine: 00:10.908, Memory: 66.00 MEAl 11 new and existing testParseContact* tests pass. The deprecations are pre-existing PHPUnit warningsLet ma aiso ar'cklv nin tha mll SarufcaTast to contmm nothico ie bzokna.• decter exe sdrcker /era/3 bestore Servioeeeitetnay 55 php a5vendor/bin/phpun:PHPUnzt 11.5.55 by Sebastzan Beramann and contributors.Confiquration:/home/siminny/phpunit.xmD.•DODOD..•••D000.DD.......••• 65 / 73 ( 894TIeA: 80.2 70 MARAPus OA 1a0 MpA.7s tests pass. Herc's a summary of what was added:oo eoesThe PR already covered empty/null early-return paths. The missing coverage was for the happy paths - added 7newssanrsctontact phos•testParseContactPhoneWithValidNumber - E.164 number parses correctly, ext is nul?+eParsccontad Pnonewnextens ton -Tmoer wihler dcontoty ertactne exentotactpareacontsctPhonaFyttetruncatettotochare=sytwttolonosthm.th/chhre.abtetnindstodllGtru1taetbareacontnetPhondthrount.rtodesloclkoim.tthto.totowmeine.cotntrv.conparseContactMobile•testParsetontac Nonathaidduher- E6 numaertetumed netActPsreAtontsctwahsntehrunestMhT/20Ghhre-Mimawtlwawthan2f.thare aote thinastodltroledtACtOsrcAGont.CMAKSlWWAthGAuAtruCAA.-TAAITIKmAwAroALAhtArmcimaAMlintrv.oh1tile +80 2Accept allAsk anything (XoL)" PodnAdhotvwodturlhhmeewhi.es*4 space...
|
85728
|
NULL
|
NULL
|
NULL
|
|
85729
|
2938
|
3
|
2026-05-28T13:03:12.948247+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973392948_m1.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.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
|
idle
|
hybrid
|
NULL
|
Project: faVsco.js, menu
iTerm2• • 0ShellEditVie Project: faVsco.js, menu
iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKER-₴81DEV (docker)₴82-zshN3ffmpegO ₴4-zshapp/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php348+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++*app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec-it docker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|85ec2-user@ip-10-30-129-...100% <78• Thu 28 May 16:03:121₴1ec2-user@ip-10-30-140-...₴7...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85728
|
2939
|
4
|
2026-05-28T13:03:11.614822+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973391614_m2.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.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}]...
|
8043719072324535154
|
-8628527368849355612
|
idle
|
hybrid
|
NULL
|
Project: faVsco.js, menu
rapstomViewCoocWindowFV f Project: faVsco.js, menu
rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-• HttpIntegrationslinteractionsE Jobsa MailModelsanoureatonsvoseivers(C SocialAccountObserverTest.gUserObserverTest.phpUserRoleObserverTest.phoPolcies>EACtVIYDa ActivityProviderslealendanTeonterenco•RullmeencloseleooneMi HibsootlllistenarsPipedrive(D Salesforce• (b Field:D OpportunityMatcherOpportunitySyncStratel ProsoectSearchStratecim sancettaisClientTest.phoC DecorateActivityTest.oC DeleteObiectsTrait Testananafaiioostestoh© GetActivitvFieldNameTPavloadBuilder [EMAIL]© QuervHandlerTest.oha©QuerviteratorTest.ohr©QuervResuitsTests.ohC Semceltestoo@SwncRatchRedicSenics@ RaseSeniiceTest ohownapers.ohyTeom.one38 %9©ActivityControlier.phpuCicheoneTO0У L7inu co moy to.us.+0.A SF fiminny@localhost)console (PROD) XA console (STAGING)De000T Autowreated at > DATE SUB(NOWO), INTERVAL 38 DAYBY u.id, u.email,u.name u.sotcphone nunbenBY sns_count DESC;7827es* fron teans where id = 1;710711=712713NCAT(u.id, CASE WHEN U.id = t.ouner_id THEN ' (ouner) ' ELSE "* END) AS user_id,Curssirvcaloslexcondsesstpublic function testFetchAndAssociateRelatedActivityO: voidSatient & Sthis->createlockld onenettenteclass)sSoav loadBusi den & SthisoscrenteVockld cno neteiosePayloadBust den. sclassh.SoavloadBusi dens>nethodid constn->willReturnCallback(function (Sactivity, Spayload) (...*)ScountriesMap = Sthis->createMock origl/** @var Service&MockObnect SserviceMock */SserviceMock = Sthis->getMockBuilder className: Service::class)->setConstructorArgs(CalkontSoayLoadbuzloersevencurspaccherScountriesMapSthis->createMock( originalClassh>oncynechodstecchketacecacc.v.cy→>аскносkонwcthoekoseyocerReiaho>oneo>mhodlTerchie fred.cewiy'-sweeerumnia"descriintsion' "Test descriattion"Steam = Sthis->createMock( originTean::class):"aetattoibute"oythltearoum"tean"oyil Return SteamplavhonkecllaseSnlavhonkesnethodlTon.offedtradend v Accept Fle x- 5 X-Papaи fнo p3elnninhilsSplaybook->method( constraint: "getActivityType')->willReturn( value: Playbook::ACTIVITY TYPE EVENT) =acrviies a UN to.acovity 1d = a.10vider1d = 'hs actavity tyoen* FROM activity_messages:fron text rellavs where created.ar > 7826-85-8* fron actsivities where user id TN (7168, 18688) and created at > *2826-85-22' order by sid descl* fron users where team id = 1 and id TN (18688, 13934. 7169)=* fron activities where user_id = 7160 order by id desc Linit 10;* fron accounts where team_id = 1 and nane = 'Columns":*Toon tasne whendae* fron actávity_searches where user_id = 31854;*Eoon sctkusty coanch Eilitane nhans actkusty cancch fa TM CRRRR2 RRO02# console fiaumCascadillesorcesarici045 A1 A41 У 66 4SanEelton etnctParcaContncta 256could not ooen snout ote: var wwlntevarttsantester exedrcker/crm/5 lestore Sevicetest.ptp -f ite artestParseContact' 2261couid nor doen sinmt retarsa• docker ps =format "(.Names)}' 2261 || head -20docker-sorsadb-1° docker exec docker_lamp_1 bash -C "find / -nane "artisan' -saxdepth 6 2>/dev/null | heahosa/henoulartendeske/unteC serckers/cre/3 testore vServicerest. Prpy Ptere nesr/arsecontact: 2-61-PHPUnit 11.5.55 by Sebastian Bergmann and contributors.11 / 11 (100%Tise: 08:10.998. Menory: 66.00 MEAl 11 new and existing testParseContacte tests pass. The deprecations are pre-existing PHPUnit warningsunelatooucnandeset ma siea ai ntlv nin tho tuli Carus taTaet to ran tmm nathinh te maieio decker exec sdocker lamp 3 bash -e Sertoee/stainny 56 php vend020vendor/bin/phpuni1tile +80 2Accept allAsk anything (XOL)" PodnAdhotv• Owodturlhhmeewhi.est4 spad...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85727
|
2938
|
2
|
2026-05-28T13:02:39.348567+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973359348_m1.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2• • 0ShellEditViewSessionScripts|ProfilesW iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKER₴81DEV (docker)₴82-zshN3screenpipe"O ₴4-zshapp/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php348+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec-it docker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|85ec2-user@ip-10-30-129-...100% <78• Thu 28 May 16:02:381₴1ec2-user@ip-10-30-140-...₴7...
|
NULL
|
4587511657139576741
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2• • 0ShellEditViewSessionScripts|ProfilesW iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKER₴81DEV (docker)₴82-zshN3screenpipe"O ₴4-zshapp/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php348+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec-it docker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|85ec2-user@ip-10-30-129-...100% <78• Thu 28 May 16:02:381₴1ec2-user@ip-10-30-140-...₴7...
|
85724
|
NULL
|
NULL
|
NULL
|
|
85726
|
2939
|
3
|
2026-05-28T13:02:36.662551+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973356662_m2.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20 rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-lnhe heloers phialermninete th oe©ActivityControlier.php8 HtteIntegrationsnlinteractionsuewholeooserver.onyTeom.oneuCicheonea Jobsauisteneisa MailModelsanoureadonsvosewvers(C SocialAccountObserverTest.rUserObserverTest.phpUserRoleObserverTest.phoPolciesa ProviderE Ruc.>EACtVIYDa ActivityProviderslealendan•Tenterencoa erm→RulihorcloseleooneTmermob actsHelpersHubspotD IntegrationAppTn l istenars1114VNu PioconiveMSalestorce1182 dIMEeOSD OpportunityMatcher1228 €l OpportunitySyncStratel ProsoectSearchStratecim sancettaisClientTest.oho1366CwsamtosdweC DeleteObiectsTrait Testaanhtntoasieron© GetActivitvFieldNameTPavloadBuilder [EMAIL]© QuervHandlerTest.oha©QuerviteratorTest.ohr©QuervResuitsTests.ohe1592 6C Semceltestoo@*SuncRatchRedicSenics@ RaseSeniiceTest ohoprivate function createMockedConfiguration: Configuration(...;Z0 090005private function getServiceMock(array SonlyMethods = (): MockObject&Service(...-private function mockQueryBuilderAndHandler(SuserToSearch, SsalesforceUsers): voidk...-Tuser suser = nultarray Scustonkules =): void {..11 usageprivate function nockProfileRepository(User Suser): voidt...)public function testBuildEnhancedNoteDecodesHtmlEntztzes: voidt...=public function testBuildEnhancedNoteSanitizeslithoutQuotes(): voidf...}public function testBuildEnhancedNoteConvertsLineBreaksO: voidt...public function testBuildEnhancedNoteHandLesCompLexScenario: voidt...:public function testSyncRenotelyDeleted0bfectsllithErrorHandLingSuccess(): voidf...}public function testSyncRemotelyDeleted0biectsllithErrorHandLingFailure): voidi...}public function testSyncRemotelyDeleted0biectsltithErrorHandLingWithLogParans: voidf...}* AdataProvider cra0biectProvidenpublic functaion testSyncRenotelv0eleted0biectshithErrorHandTing0ifferentCon0biects@.Jiminny\Enums\Co101oublic funetsion testHandle0btectDeletionltithDeletedEntitvO= voidh.public functaion testHandle0bdectDeletzioniithMonDeletedEntatyOe voidi...}oublie funetzion testDeleteCon0bdectuithValidentty@e vosdf...%public function testDeretecrnübjectiis v Accept Fle x-3XORENet-Fie OX@TO0У L7Thu 28 May 16:02:36+0.A SF fiminny@localhost)console (PROD) XA console [STAGING)100 €Ty: Autow78178210%=7715716ДНШНО ПИПВН-НННВНОНОПЛИВНИЕ ИНВ В-712)=INTERVAL 38 DAY.BY u.id, u.email, u.name, u.softphone numben* fron teans where id = 1;" tron roleshNCAT(u.id, CASE WHEN u.id = t.ouner_id THEN • (ouner)* ELSE ** END) AS user_idouner_id FROM social_accounts saeans t 1n<->1:on t.id= u.tean jdU.tean_id = 1117 and sa.provider = "hubspot":*EROM ActM tes WHEOF Wund to binl18A2tetth-2147-1817.0144-40480695926801= Musd: * 70033459 V59*EROM ActM tes WHEOF muhd to binl:255208/3-800/-1781-0276.66648288185c01= musde 80186192 M0* FROM crn_configurations WHERE id = 1853)* Eoon uicene whena 3d = 30240* Eoon nlavbonke whane 1d = 5/775* fron playbook categonies where id = 43783:* fron playbook_categories nhere playbook_id = 5473;*Eoon enn ffalde whons 4d = 450242.*foon en £4old valuae whans con £iald 4d = 450242.* FROM Crn_field_dato fdAAE RAA GOM EH AN CANWRACEEacrviies a UN to.acovity 1d = a.10crnprowiderd= hs actvity type"* FROM activity_messages:where created at > *2826-05-91*•* fron actsivities where user_id TN (7168, 18688) and created at > '2826-85-22' order by sid descausers where team id = 1 and id TN (18688, 13934. 7169):*£ron activities where usen 1a = 7169 order bu ja desc 1ini+ 18:* Aron accounts whene taar oe and nane wallease*£oon usens whene name 13ke IXSubrakt. n 31954 1112Toon tasne whanda4*foon Antéusty seanches whene usen $a = 71852.*foon Antsusty seanch filtene shere actsusty eenoch Sa TM (RRRR) RROp2).# console fiaum045 A1 A41 У 66 4Cascaollestorcesaricet:wwrccontac.thone withatenl onone.nomootnotau.thowihaoonenomochatnaanextcnsonwrttontc wobe within renl mooie numoer. with trinetohltyt charsNow let me verity the tests run. First let me find the right test runner commanddests/unxe/ erciees/crm/Sa lestorce/ServiceTesttison -fiitere"testParseContact" 2>61coullo no ooensinoeeewrwwneiret• docker exec docker lamp 1 bash -c "cd /var/ww/html 66 php artisan testtests/Unit/Services/Crm/Salesforce/ServiceTest.php =f1lter='testParseContact' 2>&1"could not ooen snout ote: artisa• docker ps -format "{{.Names)}' 2>61 | head -20elasticsearchdocker-marindb-1• docker exec docker_lamp_1 bash -C "find / -nane "artisan' -saxdepth 6 2>/dev/null | heahore/imsinny/artatsaresker exe serckers/cra73 bestore eServicee te phpy -f te re resr/arsecontact' 2-61°PHPUnit 11.5.55 by Sebastian Bergmann and contributors.OUD 9 CCconsourctontno.ebttnay/ohounx1/1111004Tiee: 00:10.908, Menory: 66.00 MEAl 11 now and exicting testparsctontacte toate onke. tihe doorcantions aae or1tile +80 2Accept allAsk anything (XOL)" PodeAdhotvwodturlhhmeewhi.est4 spad...
|
NULL
|
169212733282570035
|
NULL
|
visual_change
|
ocr
|
NULL
|
rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20 rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-lnhe heloers phialermninete th oe©ActivityControlier.php8 HtteIntegrationsnlinteractionsuewholeooserver.onyTeom.oneuCicheonea Jobsauisteneisa MailModelsanoureadonsvosewvers(C SocialAccountObserverTest.rUserObserverTest.phpUserRoleObserverTest.phoPolciesa ProviderE Ruc.>EACtVIYDa ActivityProviderslealendan•Tenterencoa erm→RulihorcloseleooneTmermob actsHelpersHubspotD IntegrationAppTn l istenars1114VNu PioconiveMSalestorce1182 dIMEeOSD OpportunityMatcher1228 €l OpportunitySyncStratel ProsoectSearchStratecim sancettaisClientTest.oho1366CwsamtosdweC DeleteObiectsTrait Testaanhtntoasieron© GetActivitvFieldNameTPavloadBuilder [EMAIL]© QuervHandlerTest.oha©QuerviteratorTest.ohr©QuervResuitsTests.ohe1592 6C Semceltestoo@*SuncRatchRedicSenics@ RaseSeniiceTest ohoprivate function createMockedConfiguration: Configuration(...;Z0 090005private function getServiceMock(array SonlyMethods = (): MockObject&Service(...-private function mockQueryBuilderAndHandler(SuserToSearch, SsalesforceUsers): voidk...-Tuser suser = nultarray Scustonkules =): void {..11 usageprivate function nockProfileRepository(User Suser): voidt...)public function testBuildEnhancedNoteDecodesHtmlEntztzes: voidt...=public function testBuildEnhancedNoteSanitizeslithoutQuotes(): voidf...}public function testBuildEnhancedNoteConvertsLineBreaksO: voidt...public function testBuildEnhancedNoteHandLesCompLexScenario: voidt...:public function testSyncRenotelyDeleted0bfectsllithErrorHandLingSuccess(): voidf...}public function testSyncRemotelyDeleted0biectsllithErrorHandLingFailure): voidi...}public function testSyncRemotelyDeleted0biectsltithErrorHandLingWithLogParans: voidf...}* AdataProvider cra0biectProvidenpublic functaion testSyncRenotelv0eleted0biectshithErrorHandTing0ifferentCon0biects@.Jiminny\Enums\Co101oublic funetsion testHandle0btectDeletionltithDeletedEntitvO= voidh.public functaion testHandle0bdectDeletzioniithMonDeletedEntatyOe voidi...}oublie funetzion testDeleteCon0bdectuithValidentty@e vosdf...%public function testDeretecrnübjectiis v Accept Fle x-3XORENet-Fie OX@TO0У L7Thu 28 May 16:02:36+0.A SF fiminny@localhost)console (PROD) XA console [STAGING)100 €Ty: Autow78178210%=7715716ДНШНО ПИПВН-НННВНОНОПЛИВНИЕ ИНВ В-712)=INTERVAL 38 DAY.BY u.id, u.email, u.name, u.softphone numben* fron teans where id = 1;" tron roleshNCAT(u.id, CASE WHEN u.id = t.ouner_id THEN • (ouner)* ELSE ** END) AS user_idouner_id FROM social_accounts saeans t 1n<->1:on t.id= u.tean jdU.tean_id = 1117 and sa.provider = "hubspot":*EROM ActM tes WHEOF Wund to binl18A2tetth-2147-1817.0144-40480695926801= Musd: * 70033459 V59*EROM ActM tes WHEOF muhd to binl:255208/3-800/-1781-0276.66648288185c01= musde 80186192 M0* FROM crn_configurations WHERE id = 1853)* Eoon uicene whena 3d = 30240* Eoon nlavbonke whane 1d = 5/775* fron playbook categonies where id = 43783:* fron playbook_categories nhere playbook_id = 5473;*Eoon enn ffalde whons 4d = 450242.*foon en £4old valuae whans con £iald 4d = 450242.* FROM Crn_field_dato fdAAE RAA GOM EH AN CANWRACEEacrviies a UN to.acovity 1d = a.10crnprowiderd= hs actvity type"* FROM activity_messages:where created at > *2826-05-91*•* fron actsivities where user_id TN (7168, 18688) and created at > '2826-85-22' order by sid descausers where team id = 1 and id TN (18688, 13934. 7169):*£ron activities where usen 1a = 7169 order bu ja desc 1ini+ 18:* Aron accounts whene taar oe and nane wallease*£oon usens whene name 13ke IXSubrakt. n 31954 1112Toon tasne whanda4*foon Antéusty seanches whene usen $a = 71852.*foon Antsusty seanch filtene shere actsusty eenoch Sa TM (RRRR) RROp2).# console fiaum045 A1 A41 У 66 4Cascaollestorcesaricet:wwrccontac.thone withatenl onone.nomootnotau.thowihaoonenomochatnaanextcnsonwrttontc wobe within renl mooie numoer. with trinetohltyt charsNow let me verity the tests run. First let me find the right test runner commanddests/unxe/ erciees/crm/Sa lestorce/ServiceTesttison -fiitere"testParseContact" 2>61coullo no ooensinoeeewrwwneiret• docker exec docker lamp 1 bash -c "cd /var/ww/html 66 php artisan testtests/Unit/Services/Crm/Salesforce/ServiceTest.php =f1lter='testParseContact' 2>&1"could not ooen snout ote: artisa• docker ps -format "{{.Names)}' 2>61 | head -20elasticsearchdocker-marindb-1• docker exec docker_lamp_1 bash -C "find / -nane "artisan' -saxdepth 6 2>/dev/null | heahore/imsinny/artatsaresker exe serckers/cra73 bestore eServicee te phpy -f te re resr/arsecontact' 2-61°PHPUnit 11.5.55 by Sebastian Bergmann and contributors.OUD 9 CCconsourctontno.ebttnay/ohounx1/1111004Tiee: 00:10.908, Menory: 66.00 MEAl 11 now and exicting testparsctontacte toate onke. tihe doorcantions aae or1tile +80 2Accept allAsk anything (XOL)" PodeAdhotvwodturlhhmeewhi.est4 spad...
|
85725
|
NULL
|
NULL
|
NULL
|
|
85725
|
2939
|
2
|
2026-05-28T13:02:31.332664+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973351332_m2.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20 rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-proideta cayicepnnhe heloers phitnelereobiecistraicon:©ActivityControlier.php• HttpIntegrationslinteractionsuewholeooserver.ono Teanupc) e enupn;E Jobsclass ServiceTest extends TestCasea MailModelsNotifications24922493 K24942495public function testResolveContactCountryCode(array ScrnData,?bool ScountryExistsvoseivers@ SocialAccountObserverTest, 2497UserObserverTest.php2470AEZAUserRoleObserverTest.phoasuans sconvercedcoderocrans sexoected: void ..Polcies1Usdotpublic static function parseContactPhoneProvider@: array..254425452540*soratnov der parseconcac.Phonerrovzden>EACtVIYDa ActivityProviders2548 ₽public function testParseContactPhoneWithEmotyPhone(array Scrabata, Pstring ScountryCode, array Sexo6a 7145SSlealendanpubazc tuncicion testParsetoncac Phoncazchva-zohunber: vozdTeonterenco7557a erm•Rullmeen& CloseleooneTi HelnersHubspotD IntegrationApplllistenars256425652566u Pioconive(D Salesforce2567 D• (b Fields2568D OpportunityMatcher2569OpportunitySyncStrateD ProsoectSearchStratecim sorcettaisClientTest.phoC DecorateActivityTest.o 45/4DeleteObiectsTraitTest 2Aaanhtntoasieron@ GetActivitvFieldNameT [EMAIL]© QuervHandlerTest.oha© Querviterator Test.oho©QuervResuitsTests.ohepublic function testParseContactPhoneExtIsTruncatedTo10Chars@: voidSservice = Sthis->aerservtcrhock onSermlara= "Phone" =>•[PHONE] ext. 1234567890101C Semcelteston@SwncRatchRedicSenics@ RaseSeniiceTest oho(* CacharCrmSanvirehem 258,st:t::nvokeprvoreherhodldSthis->assertNotNuT (Sosct)ditsv Aocepkhie xeX Reject File [EMAIL] SF fiminny@localhost)console (PROD) Xconsole (STAGING)7827es•104718T AutowINTERVAL 38 DAY.BY u.id, u.email, u.name, u.softphone numben* fron teans where id = 1;NCAT(u.id, CASE WHEN U.id = t.ouner_id THEN ' (ouner)' ELSE ** END) AS user_id,nen o FRil soctallaccauntssarGTRunCosSontd M.teand*EROM ActM tes WHEOF Wund to binl18A2tetth-2147-1817.0144-40480695926801= Musd: * 70033459 V59actavi tes WHEoE mund to binl:255200/3-8001-1781-0276.66648288185c0= Musde 80186192 Mn* FROM crn_configurations WHERE id = 1853)# console fiaum045 A1 A41 У 66 A* FROM cra_field_data fdAAE ERAAGOM CAAA CAAWSAS6Nacrviies a ol to,aciavity 1d = a.10* fron text relays where created at > *2026-85-91*-* fron actsivities where user id TN (7169, 18688) and created at > '2826-85-22* order by sid desc* fron users where teamsid = 1 and Sid TN (18698, 13934- 2149)-* fron activities where user_id = 7160 order by id desc linit 10;Toon tasne whanda4*foon Antsusty seanch filtene shere actsusty eenoch Sa TM (RRRR) RROp2)TO0У L7Thu 28 May 16:02:30CascaoSllestorcesarice+0.1. parseContactPhone with a real phone number (no ext), and with a phone number that has an extensionmrsciontactnooile wih areal moolle numcer, Witi truncacon 2ho coars.Servicerest.php+80Now ict me verily the tests run. First let me tind the nght test runner commanddester exe/ serckees/cre/5o lsforee/ServiceT/st-pipn -Filtere-testParseContact" 261Could not open input file: /var/www/htmlartisano desker exet deckcr-/am /3 testo re /Se actest pip -f, pte ra testr rsetontact 2561Could not open input file: artisan• docker ps —fomst off.Naresll: 2261 ll hesd -20Bocker-asriadb-1• docker exec docker_lamp_1 bash -c "find / -nane "artisan' -eaxdepth 6 2>/dev/null | heahoseseonslhrttf ptervenest/arsecontact' 2-61*PHPUnit 11.5.55 by Sebastian Bergeann and contributors.Runtime:tion. PHP 8.5.5/now/chounte.11/ 11 (1084Tire: 08:10,988, Memory: (66,80 ME1tile +80 2Ask anything (XOL)AdhotvAcceot all• Owodturlhhmeewhi.est4 spad...
|
NULL
|
-786771730219802476
|
NULL
|
idle
|
ocr
|
NULL
|
rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20 rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-proideta cayicepnnhe heloers phitnelereobiecistraicon:©ActivityControlier.php• HttpIntegrationslinteractionsuewholeooserver.ono Teanupc) e enupn;E Jobsclass ServiceTest extends TestCasea MailModelsNotifications24922493 K24942495public function testResolveContactCountryCode(array ScrnData,?bool ScountryExistsvoseivers@ SocialAccountObserverTest, 2497UserObserverTest.php2470AEZAUserRoleObserverTest.phoasuans sconvercedcoderocrans sexoected: void ..Polcies1Usdotpublic static function parseContactPhoneProvider@: array..254425452540*soratnov der parseconcac.Phonerrovzden>EACtVIYDa ActivityProviders2548 ₽public function testParseContactPhoneWithEmotyPhone(array Scrabata, Pstring ScountryCode, array Sexo6a 7145SSlealendanpubazc tuncicion testParsetoncac Phoncazchva-zohunber: vozdTeonterenco7557a erm•Rullmeen& CloseleooneTi HelnersHubspotD IntegrationApplllistenars256425652566u Pioconive(D Salesforce2567 D• (b Fields2568D OpportunityMatcher2569OpportunitySyncStrateD ProsoectSearchStratecim sorcettaisClientTest.phoC DecorateActivityTest.o 45/4DeleteObiectsTraitTest 2Aaanhtntoasieron@ GetActivitvFieldNameT [EMAIL]© QuervHandlerTest.oha© Querviterator Test.oho©QuervResuitsTests.ohepublic function testParseContactPhoneExtIsTruncatedTo10Chars@: voidSservice = Sthis->aerservtcrhock onSermlara= "Phone" =>•[PHONE] ext. 1234567890101C Semcelteston@SwncRatchRedicSenics@ RaseSeniiceTest oho(* CacharCrmSanvirehem 258,st:t::nvokeprvoreherhodldSthis->assertNotNuT (Sosct)ditsv Aocepkhie xeX Reject File [EMAIL] SF fiminny@localhost)console (PROD) Xconsole (STAGING)7827es•104718T AutowINTERVAL 38 DAY.BY u.id, u.email, u.name, u.softphone numben* fron teans where id = 1;NCAT(u.id, CASE WHEN U.id = t.ouner_id THEN ' (ouner)' ELSE ** END) AS user_id,nen o FRil soctallaccauntssarGTRunCosSontd M.teand*EROM ActM tes WHEOF Wund to binl18A2tetth-2147-1817.0144-40480695926801= Musd: * 70033459 V59actavi tes WHEoE mund to binl:255200/3-8001-1781-0276.66648288185c0= Musde 80186192 Mn* FROM crn_configurations WHERE id = 1853)# console fiaum045 A1 A41 У 66 A* FROM cra_field_data fdAAE ERAAGOM CAAA CAAWSAS6Nacrviies a ol to,aciavity 1d = a.10* fron text relays where created at > *2026-85-91*-* fron actsivities where user id TN (7169, 18688) and created at > '2826-85-22* order by sid desc* fron users where teamsid = 1 and Sid TN (18698, 13934- 2149)-* fron activities where user_id = 7160 order by id desc linit 10;Toon tasne whanda4*foon Antsusty seanch filtene shere actsusty eenoch Sa TM (RRRR) RROp2)TO0У L7Thu 28 May 16:02:30CascaoSllestorcesarice+0.1. parseContactPhone with a real phone number (no ext), and with a phone number that has an extensionmrsciontactnooile wih areal moolle numcer, Witi truncacon 2ho coars.Servicerest.php+80Now ict me verily the tests run. First let me tind the nght test runner commanddester exe/ serckees/cre/5o lsforee/ServiceT/st-pipn -Filtere-testParseContact" 261Could not open input file: /var/www/htmlartisano desker exet deckcr-/am /3 testo re /Se actest pip -f, pte ra testr rsetontact 2561Could not open input file: artisan• docker ps —fomst off.Naresll: 2261 ll hesd -20Bocker-asriadb-1• docker exec docker_lamp_1 bash -c "find / -nane "artisan' -eaxdepth 6 2>/dev/null | heahoseseonslhrttf ptervenest/arsecontact' 2-61*PHPUnit 11.5.55 by Sebastian Bergeann and contributors.Runtime:tion. PHP 8.5.5/now/chounte.11/ 11 (1084Tire: 08:10,988, Memory: (66,80 ME1tile +80 2Ask anything (XOL)AdhotvAcceot all• Owodturlhhmeewhi.est4 spad...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85724
|
2938
|
1
|
2026-05-28T13:02:26.814901+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973346814_m1.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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":"TextRelayServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","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}]...
|
1962564511686315741
|
1735522336325714144
|
idle
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKERO ₴1DEV (docker)₴82-zshN3screenpipe"O &4-zshapp/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php348+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec-it docker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|85100% <78• Thu 28 May 16:02:26L881ec2-user@ip-10-30-129-...ec2-user@ip-10-30-140-...₴7...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85723
|
2939
|
1
|
2026-05-28T13:01:59.196126+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973319196_m2.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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.8374335,"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":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-5714658179048184897
|
-8634297297851446848
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-Proinet va cayicepnnhe heloers phitnelereobiecistraicon:©ActivityControlier.php• HttpIntegrationslinteractionsuewholeooserver.ono Teanupc) e enupn;Jobsclass ServiceTest extends TestCasea MailModelsNotifications2492249324942495public function testResolveContactCountryCode(array ScrnData,?bool ScountryExistsvoseivers@ SocialAccountObserverTest, 2497UserObserverTest.php2470AEZAUserRoleObserverTest.phoasuans sconvercedcoderocrans sexoected: void ..Polciespublic static function parseContactPhoneProvider@: array…….254425452540*sorarnovden parseconcac.Fhonerrovzden>EACtVIYDa ActivityProviders2548 ₽public function testParseContactPhoneWlithEmotyPhone(array ScreData, ?string ScountryCode, array Sexo6a 7145SSlealendanpubac tuncicion tes Parsecontac Phoncuzchva zokunber: vozdTeonterenco7557a erm•Rullmeen& CloseleooneTin HelnersHubspotD IntegrationApplllistenars256425652566u Pioconive(D Salesforce2567• (b Fields2568D OpportunityMatcher2569OpportunitySyncStrateD ProsoectSearchStratecim sancettaisClientTest.phoC DecorateActivityTest.o 45/4DeleteObiectsTraitTest 2Aaanhtntoasieron© GetActivitvFieldNameT [EMAIL]© QuervHandlerTest.oha© Querviterator Test.oho©QuervResuitsTests.ohepublic function testParseContactPhoneExtIsTruncatedTo10CharsO: voidSservice = sthis->gerServzcrhock orScrmlata = "Phone' =>•[PHONE] ext. 1234567890101C Semceltestoo@SwncRatchRedicSenics@ RaseSeniiceTest oho(* CacharCrmSanvirehem 258,selt::nvokeprvereherhodkdSthis->assertNotNuT (Sosct)ditsv Aocepkhie xeX Reject File [EMAIL] SF fiminny@localhost)console (PROD) Xconsole (STAGING)7827es•104718T AutowINTERVAL 38 DAY.BY u.id, u.email, u.name, u.softphone numben* fron teans where id = 1;NCAT(u.id, CASE WHEN U.id = t.ouner_id THEN ' (ouner)' ELSE ** END) AS user_id,nen fo Fril soctal accauntssarGTRunCosSontd M.teand*EROM ActM tes WHEOF Wund to binl18A2tetth-2147-1817.0144-40480695926801= Musd: * 70033459 V59actavi tes WHEoE mund to binl:255200/3-8001-1781-0276.66648288185c0= Musde 80186192 Mn* FROM crn_configurations WHERE id = 1853)# console fiaum045 A1 A41 У 66 4* FROM cra_field_data fdAAE ERAAGOM CAAA CAAWSAS6Nacrviies a UN to.acovity 1d = a.10* fron text relays where created at > +2026-05-91*-* fron actsivities where user id TN (7169, 18688) and created at > '2826-85-22* order by sid desc* fron users where teamsid = 1 and Sid TN (18698, 13934- 2149)-* fron activities where user_id = 7160 order by id desc linit 10;TO0У L7inu co mey toulros+0.that gets parsed, and when there's an extension. Let me also check the TestPrivateMethod trait and the parsephoc 0164 helperahorast Contextexplored helpers.php and scarched 3 queriesNowlnavecwerwninolneo..tih Phes cxisinows.onlycoltneemo.wnullllcan.were.urn.or.ns.o.noarsecontatPhone and parseContactMobite.lneed to adoT. persccontactphone with a feal phone nomoer (no cau, ano witi & phione nomoer tnat nas an extenstonparsecontactMobile with a real mobile number, with truncation (>75 charsServiceTest.phpowhe me venythe tests mn.firstlet me ind the nont toet moner commands destsr extcsdrckers/crm/Soles orce/servitetest.han -fittere"testParseContact™ 2262Could not open input file: /var/www/htmlartisatester exes deckee /ar/3 best re Sevycetest.php -f, fte ra testparsscontact' 2261*Coulo nor oochnouenter anosd• docker ps -format "<(.Names))' 2>61 | head -20docker-sarjadb-1° docker exec docker_lamp_l bash =c "find / -nane "artisan' -naxdepth 6 2>/dev/null | headhose/1minny/arttsano desterexe drckee/er/3 lest re Servicerestephpy f ite vetestParsecontact' 2261°Toon tasne whanda4*foon Antsusty seanch filtene shere actsusty eenoch Sa TM (RRRR) RROp2)1tile +80 2Ask anything (XoL)" HodhAdhotvAccept allIModeuralaimeehiest4 spad...
|
85722
|
NULL
|
NULL
|
NULL
|
|
85722
|
2939
|
0
|
2026-05-28T13:01:54.719235+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973314719_m2.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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.8374335,"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":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"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 'TextRelayServiceTest'","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 'TextRelayServiceTest'","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}]...
|
-5599254006743683834
|
-7050965352302034496
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-proideta cayicepnnhe heloers phitnelereobiecistraicon:©ActivityControlier.php• HttpIntegrationslinteractionsuewholeooserver.ono Teanupc) e enupn;Jobsclass ServiceTest extends TestCasea MailModelsNotifications24922493 K24942495public function testResolveContactCountryCode(array ScrnData,?bool ScountryExistsvoseivers@ SocialAccountObserverTest, 2497UserObserverTest.php2470AEZA" UserRoleObserverTest.phoasuans sconvercedcoderocrans sexoected: void ..Polciespublic static function parseContactPhoneProvider@: array..254425452540*scratnovder parseconcac.Fhonerrovzden>EACtVIYDa ActivityProviders2548 ₽public function testParseContactPhoneWlithEmotyPhone(array ScreData, ?string ScountryCode, array Sexo6a 7145SSlealendanpubac tuncicion tes Parsecontac Phoncuzchva zokunber: vozdTeonterenco7557a erm•RullmeenCloseleooneTi HelnersHubspotD IntegrationApplllistenars256425652566u Pioconive(D Salesforce2567 D• (b Fields2568D OpportunityMatcher2569OpportunitySyncStrateD ProsoectSearchStratecim sorcettaisClientTest.phoC DecorateActivityTest.o 45/4DeleteObiectsTraitTest 4%aanhtntoasieron@ GetActivitvFieldNameT [EMAIL]© QuervHandlerTest.oha© Querviterator Test.oho©QuervResuitsTests.ohepublic function testParseContactPhoneExtIsTruncatedTo10Chars@: voidSservice = sthis->gerServzcrhock orScrmlata = "Phone' =>•[PHONE] ext. 1234567890101C Semceltestoo@SwncRatchRedicSenics@ RaseSeniiceTest oho(* CacharCrmSanvirehem 258,st:t::nvokeprvoreherhodldSthis->assertNotNuT (Sosct)ditsv Aocepkhie xeX Reject File [EMAIL] SF fiminny@localhost)console (PROD) Xconsole (STAGING)7827es•104718Ty: Auto wINTERVAL 38 DAY.BY u.id, u.email, u.name, u.softphone numben* fron teans where id = 1;NCAT(u.id, CASE WHEN U.id = t.ouner_id THEN ' (ouner)' ELSE ** END) AS user_id,nen fo Fril soctal accauntssarGTRunCosSontd M.teand*EROM ActM tes WHEOF Wund to binl18A2tetth-2147-1817.0144-40480695926801= Musd: * 70033459 V59actavi tes WHEoE mund to binl:255200/3-8001-1781-0276.66648288185c0= Musde 80186192 Mn* FROM crn_configurations WHERE id = 1853)# console fiaum045 A1 A41 У 66 Asles orcesamceTO0У L7inu co moy to.ulror+0.Now I have everything l need. the PR's existing tests only cover the empty/null early-return paths of parsecontactrnone and parscuontactrooste .unced to soo:1. parseContactPhone with a real phone number (no ext), and with a phone number that has an extensionmurson ac Nooe Wilhd real moote numee winttocoddctNow let me verity the tests run. first let me tind the riaht test nunner command)sockerexesdockees /arpsa paptorce/senviceTesteso Leitere*testParseContact" 255)Could not open input file: /var/www/htal/artisar•docker exec docker Nanoswloash -c "co var/mmintoluss ono artotsan tesitesre/lnit/Seruicec/ire/Sallactorce/aricalast.oowiiter"techparcetontacrlrCould not open input file: artisandocker ps -format "((.Names))' 2>8 head -20* FROM cra_field_data fdAAE ERAAGOM CHAA CAAWRACSacrviies a UN to.acovity 1d = a.10* fron text relays where created at > *2026-85-91*-* fron actsivities where user id TN (7169, 18688) and created at > '2826-85-22* order by sid desc* fron users where teamsid = 1 and Sid TN (18698. 13934- 2149)-* fron activities where user_id = 7160 order by id desc linit 10;° docker exec docker_lamp_1 bash =c "find / -name "artisan"axdeoth 6 2s/dew/nulll hoadhose/Atminny/artsantesterext docker/er/3 lestore Sericerestphp f ite etestarstContact' 2261°Pun steShinToon tasne whanda4*foon Antsusty seanch filtene shere actsusty eenoch Sa TM (RRRR) RROp2)1tile +80 2Ask anything (XoL)c HodeAdhotvAccept allwodturlhhmeewhi.est4 spad...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85721
|
2938
|
0
|
2026-05-28T13:01:54.612696+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973314612_m1.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2• • 0ShellEditViewSessionScripts|ProfilesW iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKER881DEV (docker)₴82-zshN3screenpipe"O ₴4-zshapp/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php348+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec-it docker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|85ec2-user@ip-10-30-129-...100% <78• Thu 28 May 16:01:541₴1ec2-user@ip-10-30-140-...₴7...
|
NULL
|
6482642436971305306
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2• • 0ShellEditViewSessionScripts|ProfilesW iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKER881DEV (docker)₴82-zshN3screenpipe"O ₴4-zshapp/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php348+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec-it docker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|85ec2-user@ip-10-30-129-...100% <78• Thu 28 May 16:01:541₴1ec2-user@ip-10-30-140-...₴7...
|
85720
|
NULL
|
NULL
|
NULL
|
|
85720
|
NULL
|
0
|
2026-05-28T13:01:24.496378+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973284496_m1.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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":"TextRelayServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-1678849104122188672
|
593894980799643872
|
idle
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKERO ₴1DEV (docker)₴82-zshN3screenpipe"O ₴4-zshapp/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php348+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec-it docker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|85ec2-user@ip-10-30-129-...100% <78• Thu 28 May 16:01:241₴1ec2-user@ip-10-30-140-...₴7...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85719
|
NULL
|
0
|
2026-05-28T13:01:23.321944+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973283321_m2.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20 rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-proideta cayicepnnhe heloers phitnelereobiecistraicon:©ActivityControlier.php• HttpIntegrationslinteractionsuewholeooserver.ono Teanupc) e enupn;E Jobsclass ServiceTest extends TestCasea MailModelsNotifications24922493 K24942495public function testResolveContactCountryCode(array ScrnData,?bool ScountryExistsvoseivers© SocialAccountObserverTest., 2497UserObserverTest.php2470AEZAUserRoleObserverTest.phoasuans sconvercedcoderocrans sexoected: void ..Polcies1Usdotpublic static function parseContactPhoneProvider@: array…….254425452540*sorarnovden parseconcac.Fhonerrovzden>EACtVIYla ActivityProvider:2548 ₽public function testParseContactPhoneWlithEmotyPhone(array ScreData, ?string ScountryCode, array Sexo6a 7145SSlealendanpubac tuncicion tes Parsecontac Phoncuzchva zokunber: vozdTeonterenco7557a erm•RullmeenCloseleooneTi HelnersHubspotD IntegrationApplllistenars256425652566u Pioconive(D Salesforce2567 D• (b Fields2568D OpportunityMatcher2569OpportunitySyncStrateD ProsoectSearchStratecim sancettaisClientTest.phoC DecorateActivityTest.o 45/4DeleteObiectsTraitTest 4%aanhtntoasieron© GetActivitvFieldNameT [EMAIL]© QuervHandlerTest.oha© Querviterator Test.oho©QuervResuitsTests.ohepublic function testParseContactPhoneExtIsTruncatedTo10Chars@: voidSservice = sthis->gerServzcrhock orSermlara= "Phone" =>*[PHONE] ext. 1234567890101C Semceltestoo@SwncRatchRedicSenics@ RaseSeniiceTest oho(* CacharCrmSanvirehem, 258,selt::nvokeprvereherhodkdSthis->assertNotNuT (Sosct)ditsv Aocepkhie xeX Reject File [EMAIL] SF fiminny@localhost)console (PROD) Xconsole (STAGING)7827es•104718T AutowINTERVAL 38 DAY.BY u.id, u.email, u.name, u.softphone numben* fron teans where id = 1;NCAT(u.id, CASE WHEN U.id = t.ouner_id THEN ' (ouner)' ELSE ** END) AS user_id,nen o FRil soctallaccauntssarGTRunCosSontd M.teand*EROM ActM tes WHEOF Wund to binl18A2tetth-2147-1817.0144-40480695926801= Musd: * 70033459 V59actavi tes WHEoE mund to binl:255200/3-8001-1781-0276.66648288185c0= Musde 80186192 Mn* FROM crn_configurations WHERE id = 1853)# console fiaum045 A1 A41 У 66 A* FROM cra_field_data fdAAE ERAAGOM CAAA CAAWSAS6Nacrviies a UN to.acovity 1d = a.10* fron text relays where created at > +2026-05-91*-* fron actsivities where user id TN (7169, 18688) and created at > '2826-85-22* order by sid desc* fron users where teamsid = 1 and Sid TN (18698, 13934- 2149)-* fron activities where user_id = 7160 order by id desc linit 10;TO0У L7inu co wey to.uleCascaolesorcesarice+0.keoo seivicelcatonp #ei-sueLet me see the rest of the test file, especially the helper methods and the new tests added in the PRNothave atulll oicture athe test etthe pe altondy nodk tosteter the emotyint lonses on anecevontac Phoaand parseContactMobite, but is missing the positive/happy-path tests - when there's an actual phone numberthat gets parsed, and when there's an extension. Let me also check the TestPrivatcMethod trait and the parsePhowraconexonisernonontNow I have everything I need. the PR's existing tests only cover the empty/nuli early-return paths of parseContactPhone and parseContactMobile.I need to addt.tscontactrnone with s feul onone numocr tho cau,aho nun&chione nomoer tnat nos an catchstonarsctontac Mooe withia resll mobile number. wiin rinc toonlDyescharConshThet nheNow iet me verity the toste run. Firet let me tind the riaht toat munner command)o desteraxe sovker /ara/ lcst frer/seritet/st-hpn -Filtera* testParseContact" 2061Coule no ooctnounterwarwwhearesa/www/htal 66 php artisan testtt onssCould not open input file: artisa.• docker ps -format "{{.Names))' 2261 head -20Alnetsceanrchdocker-narsadb-1o docker exee docker _lanp_1 bash -c "find / -nane "'artisan" -aaxdepth 6 2>/dev/null T heBToon tasne whanda4*foon Antsusty seanch filtene shere actsusty eenoch Sa TM (RRRR) RROp2)1tile +80 2Ask anything (XOL)" PodnAdhotvAccept allwodturlhhmeewhi.est4 spag...
|
NULL
|
2298013318402150831
|
NULL
|
idle
|
ocr
|
NULL
|
rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20 rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-proideta cayicepnnhe heloers phitnelereobiecistraicon:©ActivityControlier.php• HttpIntegrationslinteractionsuewholeooserver.ono Teanupc) e enupn;E Jobsclass ServiceTest extends TestCasea MailModelsNotifications24922493 K24942495public function testResolveContactCountryCode(array ScrnData,?bool ScountryExistsvoseivers© SocialAccountObserverTest., 2497UserObserverTest.php2470AEZAUserRoleObserverTest.phoasuans sconvercedcoderocrans sexoected: void ..Polcies1Usdotpublic static function parseContactPhoneProvider@: array…….254425452540*sorarnovden parseconcac.Fhonerrovzden>EACtVIYla ActivityProvider:2548 ₽public function testParseContactPhoneWlithEmotyPhone(array ScreData, ?string ScountryCode, array Sexo6a 7145SSlealendanpubac tuncicion tes Parsecontac Phoncuzchva zokunber: vozdTeonterenco7557a erm•RullmeenCloseleooneTi HelnersHubspotD IntegrationApplllistenars256425652566u Pioconive(D Salesforce2567 D• (b Fields2568D OpportunityMatcher2569OpportunitySyncStrateD ProsoectSearchStratecim sancettaisClientTest.phoC DecorateActivityTest.o 45/4DeleteObiectsTraitTest 4%aanhtntoasieron© GetActivitvFieldNameT [EMAIL]© QuervHandlerTest.oha© Querviterator Test.oho©QuervResuitsTests.ohepublic function testParseContactPhoneExtIsTruncatedTo10Chars@: voidSservice = sthis->gerServzcrhock orSermlara= "Phone" =>*[PHONE] ext. 1234567890101C Semceltestoo@SwncRatchRedicSenics@ RaseSeniiceTest oho(* CacharCrmSanvirehem, 258,selt::nvokeprvereherhodkdSthis->assertNotNuT (Sosct)ditsv Aocepkhie xeX Reject File [EMAIL] SF fiminny@localhost)console (PROD) Xconsole (STAGING)7827es•104718T AutowINTERVAL 38 DAY.BY u.id, u.email, u.name, u.softphone numben* fron teans where id = 1;NCAT(u.id, CASE WHEN U.id = t.ouner_id THEN ' (ouner)' ELSE ** END) AS user_id,nen o FRil soctallaccauntssarGTRunCosSontd M.teand*EROM ActM tes WHEOF Wund to binl18A2tetth-2147-1817.0144-40480695926801= Musd: * 70033459 V59actavi tes WHEoE mund to binl:255200/3-8001-1781-0276.66648288185c0= Musde 80186192 Mn* FROM crn_configurations WHERE id = 1853)# console fiaum045 A1 A41 У 66 A* FROM cra_field_data fdAAE ERAAGOM CAAA CAAWSAS6Nacrviies a UN to.acovity 1d = a.10* fron text relays where created at > +2026-05-91*-* fron actsivities where user id TN (7169, 18688) and created at > '2826-85-22* order by sid desc* fron users where teamsid = 1 and Sid TN (18698, 13934- 2149)-* fron activities where user_id = 7160 order by id desc linit 10;TO0У L7inu co wey to.uleCascaolesorcesarice+0.keoo seivicelcatonp #ei-sueLet me see the rest of the test file, especially the helper methods and the new tests added in the PRNothave atulll oicture athe test etthe pe altondy nodk tosteter the emotyint lonses on anecevontac Phoaand parseContactMobite, but is missing the positive/happy-path tests - when there's an actual phone numberthat gets parsed, and when there's an extension. Let me also check the TestPrivatcMethod trait and the parsePhowraconexonisernonontNow I have everything I need. the PR's existing tests only cover the empty/nuli early-return paths of parseContactPhone and parseContactMobile.I need to addt.tscontactrnone with s feul onone numocr tho cau,aho nun&chione nomoer tnat nos an catchstonarsctontac Mooe withia resll mobile number. wiin rinc toonlDyescharConshThet nheNow iet me verity the toste run. Firet let me tind the riaht toat munner command)o desteraxe sovker /ara/ lcst frer/seritet/st-hpn -Filtera* testParseContact" 2061Coule no ooctnounterwarwwhearesa/www/htal 66 php artisan testtt onssCould not open input file: artisa.• docker ps -format "{{.Names))' 2261 head -20Alnetsceanrchdocker-narsadb-1o docker exee docker _lanp_1 bash -c "find / -nane "'artisan" -aaxdepth 6 2>/dev/null T heBToon tasne whanda4*foon Antsusty seanch filtene shere actsusty eenoch Sa TM (RRRR) RROp2)1tile +80 2Ask anything (XOL)" PodnAdhotvAccept allwodturlhhmeewhi.est4 spag...
|
85717
|
NULL
|
NULL
|
NULL
|
|
85718
|
2936
|
12
|
2026-05-28T13:00:51.235993+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973251235_m1.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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":"TextRelayServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-5714658179048184897
|
-8634297297851446848
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKER881DEV (docker)₴82-zshN3screenpipe"O ₴4-zshapp/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php348+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec -itdocker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|85100% <78• Thu 28 May 16:00:51L81ec2-user@ip-10-30-129-...ec2-user@ip-10-30-140-...₴7...
|
85715
|
NULL
|
NULL
|
NULL
|
|
85717
|
2937
|
20
|
2026-05-28T13:00:51.134964+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973251134_m2.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.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}]...
|
8043719072324535154
|
-8628527368849355612
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
rapstomViewCoocWindowFV f Project: faVsco.js, menu
rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-proideta cayicepnnhe heloers phitnelereobiecistraicon:©ActivityControlier.php• HttpIntegrationslinteractionsuewholeooserver.ono Teanupc) e enupn;Jobsclass ServiceTest extends TestCasea MailModelsNotifications24922493 K24942495public function testResolveContactCountryCode(array ScrnData,?bool ScountryExistsvoseivers@ SocialAccountObserverTest, 2497UserObserverTest.php2470AEZA" UserRoleObserverTest.phoasuans sconvar cedcoderocrans sexoected: void ..Polciespublic static function parseContactPhoneProvider@: array…….254425452540*sorarnovden parseconcac.Fhonerrovzden>EACtVIYla ActivityProvider:2548 ₽public function testParseContactPhoneWlithEmotyPhone(array ScreData, Pstring ScountryCode, array Sexo6a 7145SSlealendanpubuzc tuncicon testParsetontac.Phoncuz chva-zohunber: vozdTeonterenco7557a erm•Rullmeen& CloseleooneTin HelnersHubspotD IntegrationApplllistenars256425652566u Pioconive(D Salesforce2567 D• (b Fields2568D OpportunityMatcher2569OpportunitySyncStrateD ProsoectSearchStratecim sorcettaisClientTest.phoC DecorateActivityTest.o 45/4DeleteObiectsTraitTest 4%aanhtntoasieron@ GetActivitvFieldNameT [EMAIL]© QuervHandlerTest.oha© Querviterator Test.oho©QuervResuitsTests.ohepublic function testParseContactPhoneExtIsTruncatedTo10Chars@: voidSservice = sthis->gerServzcrhock orScrmlata = "Phone' =>*[PHONE] ext. 1234567890101C Semceltestoo@SwncRatchRedicSenics@ RaseSeniiceTest oho(* CacharCrmSanvirehem, 258,st:t::nvokeprvoreherhodldSthis->assertNotNuT (Sosct)ditsv Aocepkhie xeX Reject File ox@hsosassent.lescttnantrscuaitdTiesareniteytTO0У L7Thu 28 May 16:00:50A SF fiminny@localhost)console (STAGING)T AutowINTERVAL 38 DAY.BY u.id, u.email, u.name, u.softphone numben7827es* fron teans where id = 1;•104718console (PROD) X# console fiaumbmhoy045 A1 A41 У 66 Asles orcesamceNow I have a full picture of the test file. The PR already adds tests for the empty/nuli cases of parseContactPhoneano parsecontacihooile, out is missing te postave napoy-pot tests = wnen there's an actual phone numoethat gets parsed, and when there's an extension. Let me also check the TestPrivateMethod trait and the parsePhoNCAT(u.id, CASE WHEN U.id = t.ouner_id THEN ' (ouner)' ELSE ** END) AS user_id,nen o FRil soctallaccauntssarGTRunCosSontd M.teand*EROM ActM tes WHEOF Wund to binl18A2tetth-2147-1817.0144-40480695926801= Musd: * 70033459 V59actavi tes WHEoE mund to binl:255200/3-8001-1781-0276.66648288185c0= Musde 80186192 Mn* FROM crn_configurations WHERE id = 1853)Now I have everything l need. tne PR's existing tests only cover the empty/null early-return paths of parsecontactrorie ono porscuontoctoo te almoto to doc1, parseContactPhone with a real phone number (no ext), and with a phone number that has an extensionparsecontactMobile with a real mobile number, with truncation (275 chars.CandcaThet nhNowle me ventythe tosts run.sirstlet me tiod the nont tost noner commandtester axes sockces/arp/3 les /rar/SerVite/st-pran -ftera" testParseContact*™ 2062Could not open input file: /var/www/htmlartisardocker exec docker tasoloash ec "Cowarwih ho ar san tesCould not open inout file: artisa:• docker ps -format "((.Names))' 2>51 | head -20* FROM cra_field_data fdAAE ERAAGOM CAAA CAAWSAS6Nacrviies a UN to.acovity 1d = a.10* fron text relays where created at > +2026-05-91*-* fron actsivities where user id TN (7169, 18688) and created .at > *2826-85-22* orden by sid desci* fron users where teamsid = 1 and Sid TN (18698, 13934- 2149)-* fron activities where user_id = 7160 order by id desc linit 10;Toon tasne whanda4*foon Antsusty seanch filtene shere actsusty eenoch Sa TM (RRRR) RROp2)o docker exec docker_lampi bash =c "find / -nane "artisanHrdenthk 2a/daw/outhanr1tile +80 2Ask anything (XoL)AdhotvAccept allwodturlhhmeewhi.est4 spad...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85716
|
2937
|
19
|
2026-05-28T13:00:43.250177+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973243250_m2.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20 rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-proideta cayicepnnhe heloers phitnelereobiecistraicon:©ActivityControlier.php• HttpIntegrationslinteractionsuewholeooserver.ono Teanupc) e enupn;Jobsclass ServiceTest extends TestCasea MailModelsNotifications24922493 K24942495public function testResolveContactCountryCode(array ScrnData,?bool ScountryExistsvoseivers© SocialAccountObserverTest., 2497UserObserverTest.php2470AEZAUserRoleObserverTest.phoasuans sconvercedcoderocrans sexoected: void ..Polciespublic static function parseContactPhoneProvider@: array..254425452540*sorarnovden parseconcac.Fhonerrovzden>EACtVIYDa ActivityProviders2548 ₽public function testParseContactPhoneWlithEmotyPhone(array ScreData, ?string ScountryCode, array Sexo6a 7145SSlealendanpubac tuncicion tes Parsecontac Phoncuzchvazokunber: vozdTeonterenco7557a erm•RullmeenCloseleooneTi HelnersHubspotD IntegrationApplllistenars256425652566u Pioconive(D Salesforce2567 D• (b Fields2568D OpportunityMatcher2569OpportunitySyncStrateD ProsoectSearchStratecim sancettaisClientTest.phoC DecorateActivityTest.o 45/4DeleteObiectsTraitTest 2Aaanhtntoasieron© GetActivitvFieldNameT [EMAIL]© QuervHandlerTest.oha© Querviterator Test.oho©QuervResuitsTests.ohepublic function testParseContactPhoneExtIsTruncatedTo10Chars@: voidSservice = sthis->gerServzcrhock orScrmlata = "Phone' =>•[PHONE] ext. 1234567890101C Sem celtestond@SwncRatchRedicSenics@ RaseSeniiceTest oho(* CacharCrmSanvirehem, 258,st:t::nvokeprvoreherhodldSthis->assertNotNuil (Sotck)aits +v Aocepkhie xeX Reject File ox@hsosassent.lescttnantrscuaitdTiesareniteytTO0У L7Thu 28 May 16:00:43+0.A SF fiminny@localhost)console (PROD) Xconsole (STAGING)7827es•104718Ty: Auto wINTERVAL 38 DAY.BY u.id, u.email, u.name, u.softphone numben* fron teans where id = 1;NCAT(u.id, CASE WHEN U.id = t.ouner_id THEN ' (ouner)' ELSE ** END) AS user_id,nen o FRil soctallaccauntssarGTRunCosSontd M.teand*EROM ActM tes WHEOF Wund to binl18A2tetth-2147-1817.0144-40480695926801= Musd: * 70033459 V59actavi tes WHEoE mund to binl:255200/3-8001-1781-0276.66648288185c0= Musde 80186192 Mn* FROM crn_configurations WHERE id = 1853)# console fiaum045 A1 A41 У 66 A* FROM cra_field_data fdAAE ERAAGOM CAAA CAAWSAS6Nacrviies a ol to,aciavity 1d = a.10* fron text relays where created at > +2026-05-91*-* fron actsivities where user id TN (7168, 18688) and created at > *2826-85-22' order by sid descl* fron users where team.id = 1 and id TN (18688, 13934. 7168)=* fron activities where user_id = 7160 order by id desc linit 10;sles orcesamcemenoovoce ieaolomeeloctraeco leia botcoloneswa iuainate evens A seoson Eae lattens Vistary:emeseeheresone tesmle, csocclly neneloce metroosnthnewolinthaPtRend Semcalest.nhoNow I have a full picture of the test file. The PR already adds tests for the empty/null cases of parseContactPhongand parseContactMobtte, but is missing the positive/hapoy-path tests = when there's an actual phone numbethat gets parsed, and when there's an extension. Let me also check the TestPrivatcMethod trait and the parsePhoExolozed heloers. ono and searched 3 queriesNow I have everything | need. The PR's existing tests only cover the empty/null early-return paths of parseContacthocanoaarsccon.ac.hoo.crneodo1. parseContactPhone with a real phone number (no ext), and with a phone number that has an extension2mrsctontactMoble with a real mobile number. with truncation (7725 chars"Naw lot ma vorit tho tocte rin. Ciret let ma tind tha roht thet ninast tamman.tester axts dockees/er2/3 ls /rer/Servitet/artphpn Filtera"testParseContact" 206)Could not ooen nout tte: var/wwwlnteVartisandocker aver docker hen1hach ae "ed huarhwuthtn te nin nrtlenn toctesunosevces uslcstoreoeiwccletod ertcrtetralesrontoooCould not ooen inout filles artisal0 docker ps -format "{{.Names)}' 2>61 | head -20Pun std ChinToon tasne whanda4*foon Antsusty seanch filtene shere actsusty eenoch Sa TM (RRRR) RROp2)1tile +80 2Ask anything (%oL)c HodeAdhotvAccept allwodturlhhmeewhi.est4 spag...
|
NULL
|
5157980762446541433
|
NULL
|
click
|
ocr
|
NULL
|
rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20 rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-proideta cayicepnnhe heloers phitnelereobiecistraicon:©ActivityControlier.php• HttpIntegrationslinteractionsuewholeooserver.ono Teanupc) e enupn;Jobsclass ServiceTest extends TestCasea MailModelsNotifications24922493 K24942495public function testResolveContactCountryCode(array ScrnData,?bool ScountryExistsvoseivers© SocialAccountObserverTest., 2497UserObserverTest.php2470AEZAUserRoleObserverTest.phoasuans sconvercedcoderocrans sexoected: void ..Polciespublic static function parseContactPhoneProvider@: array..254425452540*sorarnovden parseconcac.Fhonerrovzden>EACtVIYDa ActivityProviders2548 ₽public function testParseContactPhoneWlithEmotyPhone(array ScreData, ?string ScountryCode, array Sexo6a 7145SSlealendanpubac tuncicion tes Parsecontac Phoncuzchvazokunber: vozdTeonterenco7557a erm•RullmeenCloseleooneTi HelnersHubspotD IntegrationApplllistenars256425652566u Pioconive(D Salesforce2567 D• (b Fields2568D OpportunityMatcher2569OpportunitySyncStrateD ProsoectSearchStratecim sancettaisClientTest.phoC DecorateActivityTest.o 45/4DeleteObiectsTraitTest 2Aaanhtntoasieron© GetActivitvFieldNameT [EMAIL]© QuervHandlerTest.oha© Querviterator Test.oho©QuervResuitsTests.ohepublic function testParseContactPhoneExtIsTruncatedTo10Chars@: voidSservice = sthis->gerServzcrhock orScrmlata = "Phone' =>•[PHONE] ext. 1234567890101C Sem celtestond@SwncRatchRedicSenics@ RaseSeniiceTest oho(* CacharCrmSanvirehem, 258,st:t::nvokeprvoreherhodldSthis->assertNotNuil (Sotck)aits +v Aocepkhie xeX Reject File ox@hsosassent.lescttnantrscuaitdTiesareniteytTO0У L7Thu 28 May 16:00:43+0.A SF fiminny@localhost)console (PROD) Xconsole (STAGING)7827es•104718Ty: Auto wINTERVAL 38 DAY.BY u.id, u.email, u.name, u.softphone numben* fron teans where id = 1;NCAT(u.id, CASE WHEN U.id = t.ouner_id THEN ' (ouner)' ELSE ** END) AS user_id,nen o FRil soctallaccauntssarGTRunCosSontd M.teand*EROM ActM tes WHEOF Wund to binl18A2tetth-2147-1817.0144-40480695926801= Musd: * 70033459 V59actavi tes WHEoE mund to binl:255200/3-8001-1781-0276.66648288185c0= Musde 80186192 Mn* FROM crn_configurations WHERE id = 1853)# console fiaum045 A1 A41 У 66 A* FROM cra_field_data fdAAE ERAAGOM CAAA CAAWSAS6Nacrviies a ol to,aciavity 1d = a.10* fron text relays where created at > +2026-05-91*-* fron actsivities where user id TN (7168, 18688) and created at > *2826-85-22' order by sid descl* fron users where team.id = 1 and id TN (18688, 13934. 7168)=* fron activities where user_id = 7160 order by id desc linit 10;sles orcesamcemenoovoce ieaolomeeloctraeco leia botcoloneswa iuainate evens A seoson Eae lattens Vistary:emeseeheresone tesmle, csocclly neneloce metroosnthnewolinthaPtRend Semcalest.nhoNow I have a full picture of the test file. The PR already adds tests for the empty/null cases of parseContactPhongand parseContactMobtte, but is missing the positive/hapoy-path tests = when there's an actual phone numbethat gets parsed, and when there's an extension. Let me also check the TestPrivatcMethod trait and the parsePhoExolozed heloers. ono and searched 3 queriesNow I have everything | need. The PR's existing tests only cover the empty/null early-return paths of parseContacthocanoaarsccon.ac.hoo.crneodo1. parseContactPhone with a real phone number (no ext), and with a phone number that has an extension2mrsctontactMoble with a real mobile number. with truncation (7725 chars"Naw lot ma vorit tho tocte rin. Ciret let ma tind tha roht thet ninast tamman.tester axts dockees/er2/3 ls /rer/Servitet/artphpn Filtera"testParseContact" 206)Could not ooen nout tte: var/wwwlnteVartisandocker aver docker hen1hach ae "ed huarhwuthtn te nin nrtlenn toctesunosevces uslcstoreoeiwccletod ertcrtetralesrontoooCould not ooen inout filles artisal0 docker ps -format "{{.Names)}' 2>61 | head -20Pun std ChinToon tasne whanda4*foon Antsusty seanch filtene shere actsusty eenoch Sa TM (RRRR) RROp2)1tile +80 2Ask anything (%oL)c HodeAdhotvAccept allwodturlhhmeewhi.est4 spag...
|
85714
|
NULL
|
NULL
|
NULL
|
|
85715
|
2936
|
11
|
2026-05-28T13:00:43.338029+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973243338_m1.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2• • 0ShellEditViewSessionScripts|ProfilesW iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKER₴81DEV (docker)₴82-zshN3screenpipe"O &4-zshapp/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php348+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec-it docker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|85100% <78 • Thu 28 May 16:00:43L881ec2-user@ip-10-30-129-...ec2-user@ip-10-30-140-...₴7...
|
NULL
|
7671074253478122683
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2• • 0ShellEditViewSessionScripts|ProfilesW iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKER₴81DEV (docker)₴82-zshN3screenpipe"O &4-zshapp/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php348+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec-it docker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|85100% <78 • Thu 28 May 16:00:43L881ec2-user@ip-10-30-129-...ec2-user@ip-10-30-140-...₴7...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85714
|
2937
|
18
|
2026-05-28T13:00:11.863305+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973211863_m2.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20 rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-Proinet va cayicepnnhe heloers phitnelereobiecistraicon:©ActivityControlier.php• HttpIntegrationslinteractionsuewholeooserver.ono Teanupc) e enupn;Jobsclass ServiceTest extends TestCasea MailModelsNotifications2492249324942495public function testResolveContactCountryCode(array ScrnData,?bool ScountryExistsvoseivers© SocialAccountObserverTest., 2497UserObserverTest.php2470AEZAUserRoleObserverTest.phoasuans sconvercedcoderocrans sexoected: void ..Polciespublic static function parseContactPhoneProvider@: array..254425452540*sorarnovden parseconcac.Fhonerrovzden>EACtVIYDa ActivityProviders2548 ₽public function testParseContactPhoneWlithEmotyPhone(array ScreData, ?string ScountryCode, array Sexo6a 7145SSlealendanpubuzc tunciion testParsetontacPhoncuzchva-zohunbero: vozdTeonterenco7557a erm•RullmeenCloseleooneTin HelnersHubspotD IntegrationApplllistenars256425652566u Pioconive(D Salesforce2567• (b Fields2568D OpportunityMatcher2569OpportunitySyncStrateD ProsoectSearchStratecim sancettaisClientTest.phoC DecorateActivityTest.o 45/4DeleteObiectsTraitTest 2Aaanhtntoasieron© GetActivitvFieldNameT [EMAIL]© QuervHandlerTest.oha© Querviterator Test.oho©QuervResuitsTests.ohepublic function testParseContactPhoneExtIsTruncatedTo10Chars@: voidSservice = sthis->gerServzcrhock orScrmbata = ['Phone' => *[PHONE] ext. 12345678991073C Semceltestoo@SwncRatchRedicSenics@ RaseSeniiceTest oho(* CacharCrmSanvirehem 258,st:t::nvokeprvoreherhodldSthis->assertNotNuil (Sotck)aits +v Aocepkhie xeX Reject File ox@hsosassent.lescttnantrscuaitdTiesareniteytTO0У L7Thu 28 May 16:00:11+0.A SF fiminny@localhost)console (PROD) Xconsole (STAGING)7827es104718T AutowINTERVAL 38 DAY.BY u.id, u.email, u.name, u.softphone numben* fron teans where id = 1;NCAT(u.id, CASE WHEN U.id = t.ouner_id THEN ' (ouner)' ELSE ** END) AS user_id,nen o FRil soctallaccauntssarGTRunCosSontd M.teand*EROM ActM tes WHEOF Wund to binl18A2tetth-2147-1817.0144-40480695926801= Musd: * 70033459 V59actavi tes WHEoE mund to binl:255200/3-8001-1781-0276.66648288185c0= Musde 80186192 Mn* FROM crn_configurations WHERE id = 1853)# console fiaum045 A1 A41 У 66 4sles orcesamcecototsersstekasesasuny app oa 95l shosa Lesforce/ServiceTest.oho 1 head -10ldeclare(strict types=1)nanesoace Tests UnitServices cm SalestorcerLet me see the rest of the test file, especially the helper methods and the new tests added in the PRNow Thave a full oicture of the tost hic, tthe PR aiready ndde toate tor the emotvinul casos of anrsetontactPhoa.that gets parsed, and when there's an extension. Let me also check the TestPrivatcMethod trait and the parsePhoExolored heloers. oho and searched 3 queriesNowhave everhina noed, nhe pi's erist no tocts colm cover the emorwinulllenr weteturn onths dfe narsecootadtPhooe and parseContactMobite.lneed to addparsccontactPhone with a real phone number (no ext, and wit & phone number that nas an extenstonnarsccontactMobite with a real mobile number. with truncation (2725 chareNo let ma verty thh tocte ain Ciret lat ma iod tha roht thet mioaat oometnalCould not open input file: /var/ww/htaWartisar* FROM cra_field_data fdAAE ERAAGOM CAAA CAAWSAS6Nacrviies a UN to.acovity 1d = a.10* fron text relays where created at > +2026-05-91*-* fron activities where user id TN (7169, 18688) and created at > *2926-85-22' order by sid desc* fron users where team.id = 1 and id TN (18688, 13934. 7168)=* fron activities where user_id = 7160 order by id desc linit 10;www/htal 66 php artisan testonteti ooc,Toon tasne whanda4*foon Antsusty seanch filtene shere actsusty eenoch Sa TM (RRRR) RROp2)1tile +80 2Ask anything (XOL)AdhotvAcceot all• Owodturlhhmeewhi.est4 spad...
|
NULL
|
6454376342407637942
|
NULL
|
click
|
ocr
|
NULL
|
rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20 rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-Proinet va cayicepnnhe heloers phitnelereobiecistraicon:©ActivityControlier.php• HttpIntegrationslinteractionsuewholeooserver.ono Teanupc) e enupn;Jobsclass ServiceTest extends TestCasea MailModelsNotifications2492249324942495public function testResolveContactCountryCode(array ScrnData,?bool ScountryExistsvoseivers© SocialAccountObserverTest., 2497UserObserverTest.php2470AEZAUserRoleObserverTest.phoasuans sconvercedcoderocrans sexoected: void ..Polciespublic static function parseContactPhoneProvider@: array..254425452540*sorarnovden parseconcac.Fhonerrovzden>EACtVIYDa ActivityProviders2548 ₽public function testParseContactPhoneWlithEmotyPhone(array ScreData, ?string ScountryCode, array Sexo6a 7145SSlealendanpubuzc tunciion testParsetontacPhoncuzchva-zohunbero: vozdTeonterenco7557a erm•RullmeenCloseleooneTin HelnersHubspotD IntegrationApplllistenars256425652566u Pioconive(D Salesforce2567• (b Fields2568D OpportunityMatcher2569OpportunitySyncStrateD ProsoectSearchStratecim sancettaisClientTest.phoC DecorateActivityTest.o 45/4DeleteObiectsTraitTest 2Aaanhtntoasieron© GetActivitvFieldNameT [EMAIL]© QuervHandlerTest.oha© Querviterator Test.oho©QuervResuitsTests.ohepublic function testParseContactPhoneExtIsTruncatedTo10Chars@: voidSservice = sthis->gerServzcrhock orScrmbata = ['Phone' => *[PHONE] ext. 12345678991073C Semceltestoo@SwncRatchRedicSenics@ RaseSeniiceTest oho(* CacharCrmSanvirehem 258,st:t::nvokeprvoreherhodldSthis->assertNotNuil (Sotck)aits +v Aocepkhie xeX Reject File ox@hsosassent.lescttnantrscuaitdTiesareniteytTO0У L7Thu 28 May 16:00:11+0.A SF fiminny@localhost)console (PROD) Xconsole (STAGING)7827es104718T AutowINTERVAL 38 DAY.BY u.id, u.email, u.name, u.softphone numben* fron teans where id = 1;NCAT(u.id, CASE WHEN U.id = t.ouner_id THEN ' (ouner)' ELSE ** END) AS user_id,nen o FRil soctallaccauntssarGTRunCosSontd M.teand*EROM ActM tes WHEOF Wund to binl18A2tetth-2147-1817.0144-40480695926801= Musd: * 70033459 V59actavi tes WHEoE mund to binl:255200/3-8001-1781-0276.66648288185c0= Musde 80186192 Mn* FROM crn_configurations WHERE id = 1853)# console fiaum045 A1 A41 У 66 4sles orcesamcecototsersstekasesasuny app oa 95l shosa Lesforce/ServiceTest.oho 1 head -10ldeclare(strict types=1)nanesoace Tests UnitServices cm SalestorcerLet me see the rest of the test file, especially the helper methods and the new tests added in the PRNow Thave a full oicture of the tost hic, tthe PR aiready ndde toate tor the emotvinul casos of anrsetontactPhoa.that gets parsed, and when there's an extension. Let me also check the TestPrivatcMethod trait and the parsePhoExolored heloers. oho and searched 3 queriesNowhave everhina noed, nhe pi's erist no tocts colm cover the emorwinulllenr weteturn onths dfe narsecootadtPhooe and parseContactMobite.lneed to addparsccontactPhone with a real phone number (no ext, and wit & phone number that nas an extenstonnarsccontactMobite with a real mobile number. with truncation (2725 chareNo let ma verty thh tocte ain Ciret lat ma iod tha roht thet mioaat oometnalCould not open input file: /var/ww/htaWartisar* FROM cra_field_data fdAAE ERAAGOM CAAA CAAWSAS6Nacrviies a UN to.acovity 1d = a.10* fron text relays where created at > +2026-05-91*-* fron activities where user id TN (7169, 18688) and created at > *2926-85-22' order by sid desc* fron users where team.id = 1 and id TN (18688, 13934. 7168)=* fron activities where user_id = 7160 order by id desc linit 10;www/htal 66 php artisan testonteti ooc,Toon tasne whanda4*foon Antsusty seanch filtene shere actsusty eenoch Sa TM (RRRR) RROp2)1tile +80 2Ask anything (XOL)AdhotvAcceot all• Owodturlhhmeewhi.est4 spad...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85713
|
2936
|
10
|
2026-05-28T13:00:11.966740+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973211966_m1.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2• • 0ShellEditViewSessionScripts|ProfilesW iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKERO ₴1DEV (docker)₴82-zshN3screenpipe"O &4-zshapp/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php348+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec-it docker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|85100% <78• Thu 28 May 16:00:11L881ec2-user@ip-10-30-129-...ec2-user@ip-10-30-140-...₴7...
|
NULL
|
2029957369956806596
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2• • 0ShellEditViewSessionScripts|ProfilesW iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKERO ₴1DEV (docker)₴82-zshN3screenpipe"O &4-zshapp/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php348+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec-it docker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|85100% <78• Thu 28 May 16:00:11L881ec2-user@ip-10-30-129-...ec2-user@ip-10-30-140-...₴7...
|
85711
|
NULL
|
NULL
|
NULL
|
|
85710
|
2937
|
16
|
2026-05-28T12:59:29.748966+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973169748_m2.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rapstomEV favscojs ~ViewCooc#12121 on JY-20963-fox rapstomEV favscojs ~ViewCooc#12121 on JY-20963-fox-hС НttpIntegrationsE Interactions#JobsO MailModelsNotifications2698voseivers2689C SocialAccountObserverTest,f 2616C UserObserverTest.phpC UserRoleObserverTest.phpPolcies26152617>EACtVIYActivityProviders@ CalendarTeonterencoCrm26212623CloseCopper2625@ Helpers@ Hubspot262726282629IntegrationAppListenersPipedriveIh Salesforce263126322633• FieidsOpportunityMatcher2635OpportunitySyncStrate 2636ProspectSearchStrateg 2657@ Service Traits© ClientTest.php© DecorateActivityTest.p 2640© DeleteObjectsTraitTest 2643© FieidDefinitions Test.ph© GetActivityFieldNameT 2643E PayloadBullder Test.phy 2614E QueryBullder Test.phpC QueryHandler Test.php 2646©QuerviteratorTest. oho© QueryResultsTests.php 2648cSemcettestono© SyncBatchRedisServict 2658@ RaseSeniiceTest ohoWindowwnapers.ohyTeom.one©ActivityController.phpTO0У L7Thu 28 May 15:59:29+0.A SF (iminny@localhost)A console (PROD] X= console (STAGING)→00000Tx: AutovINTERVAL 30 DAY)BY u.id, v.enail, u.name, u.softphone_numberBY sns_count DESC;* fron teans where id = 1;NCAT(u.id, CASE WHEN U.id = t.ouner_id THEN ' (ouner)' ELSE ** END) AS user_id,ouner_id FROM social_accounts sasers u on u.id i sa. sociable_ideans t 1.n<->1:on t.id = u.tean_idU.tean_id = 1117 and sa.provider = "hubspot":* FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896850248') = uuid; # 79933459 YES* FROM activities WHERE UUid_to_Bin('[CREDIT_CARD]-9274-444da2a8185C') = Uuid; # 80186192 NO* FROM crn_configurations WHERE id = 1853;* FROM teans WHERE 1d = 1117:A console (EU?045 A1 441 X 66 ASresult = self::ánvokePrivateMethod('parseContactMobile', $service, [ScountryCode, ScreDatal):ounaeuncsion testpansetontacrtoonethlaidd, unheroe vordSservice = Sthis->getServiceMockO:ScrmData = ('MobilePhone' => *[PHONE]*]:Sresult = self::invokePrivateMethod('parseContactMobite', $service, [null, Sernbatal);Sthis->assertNotNull(Sresult);public function testParseContactMobiLeIsTruncatedTo25Chars(): voidSemce - schoocrservecchockSlongNumber = •[PHONE]345678991234*÷ScrmData = ['MobiLePhone' => $LongNumber);Sresult = self::invokePrivateMethod('parseContactMobile', $service, (null, ScrnDatal);thts-sassertNorkultSresulsthis-rassert lessthanbrequaluo, strien sresultspublic function testParseContactMobileWlithCountryCode): voidSservice = Sthis->oetSeryscelockiorSresult = self::invokePrivateMethod('parseContactMobile', Sservice, ['GB*, ScrnDatal):Sthis->assertstringStartslithehAccept Fle X-7117127337161000000 11000ОО 10008Д 1В 010П11)* FROM CMM.field_data fdcrn_ficlds f ON fd.crm field_id = f.ioactivities a ON fd.activity_id = a.idictivity_id = 79933459f.crn_provider_id = 'hs_activity_type':* FROM activity_messages:* fron text_relays where created_at > '2026-85-01';* fron activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;* fron users where team_id = 1 and id IN (18688, 13934, 7168);* fron activities where user_id = 7160 order by id desc Linit 10;* fron accounts where tean_id = 1 and nane = 'ColumnS";* fron users where name Like 'XSubrax"; # 31854, 1117* fron teans where id = 1117;* fron actávity_searches where user_id = 31854;* fron activity_search_filters nhere activity_search_id IN (88882, 88902)CascadellesorcesariceSthis->handle0bjectDeletion(Stead, Scrabata):Command col.oo ce tvrens/skas/11s/crv/S0eSt rte/Strvicrtete/paster... HAD -truncated 124 1еnoск0):scontig = sthot-contetscX.cont1gurur/eseoclassdielotion) :Now let me see the existing test file to understand its structure:Command ed, git, hoadce /uers/turts/te/uny/5e.mt.%i/ mh/5stesforce/ServiceTest.php | head -100declare(strict_types=1);nanespace Tests\Unit\Services\Crm\Salesforce;wce uainte oono 2loaen Gye let tena Vosboay:Let me seo the rest of the tost ile, especinlly the helocr methodk and the now tocte sởdod lin the PòNow l have a fuli picture of the test file. The PR already adds tests for the empty/nulli cases of parseContactPhonethat ocis parsed, and when there's an extension. Let me also check the TestPrivatcMethod trait and the parsephonalternhans aikahdlodnehtwkconteyt. oarsep.anewumber.he.cer.tuncton.detintcondlo.oneioohinExplored helpers.php and searched 3 queriesNow I have everything I need. The PR's existing tests only cover the empty/null early-return paths of parseContactPhone and parseContactMobite.l need to add:arstionthc Phone within renl anone numonr ind wrtand withh chone numeer thathas an exens onparseContactMobile with a real mobile number, with truncation (>25 chars)1453 tokengAsk anything (XOL)" HodhAdhotv• OwodturaTaame7Xelhes2 4 spac...
|
NULL
|
9216616606631420152
|
NULL
|
visual_change
|
ocr
|
NULL
|
rapstomEV favscojs ~ViewCooc#12121 on JY-20963-fox rapstomEV favscojs ~ViewCooc#12121 on JY-20963-fox-hС НttpIntegrationsE Interactions#JobsO MailModelsNotifications2698voseivers2689C SocialAccountObserverTest,f 2616C UserObserverTest.phpC UserRoleObserverTest.phpPolcies26152617>EACtVIYActivityProviders@ CalendarTeonterencoCrm26212623CloseCopper2625@ Helpers@ Hubspot262726282629IntegrationAppListenersPipedriveIh Salesforce263126322633• FieidsOpportunityMatcher2635OpportunitySyncStrate 2636ProspectSearchStrateg 2657@ Service Traits© ClientTest.php© DecorateActivityTest.p 2640© DeleteObjectsTraitTest 2643© FieidDefinitions Test.ph© GetActivityFieldNameT 2643E PayloadBullder Test.phy 2614E QueryBullder Test.phpC QueryHandler Test.php 2646©QuerviteratorTest. oho© QueryResultsTests.php 2648cSemcettestono© SyncBatchRedisServict 2658@ RaseSeniiceTest ohoWindowwnapers.ohyTeom.one©ActivityController.phpTO0У L7Thu 28 May 15:59:29+0.A SF (iminny@localhost)A console (PROD] X= console (STAGING)→00000Tx: AutovINTERVAL 30 DAY)BY u.id, v.enail, u.name, u.softphone_numberBY sns_count DESC;* fron teans where id = 1;NCAT(u.id, CASE WHEN U.id = t.ouner_id THEN ' (ouner)' ELSE ** END) AS user_id,ouner_id FROM social_accounts sasers u on u.id i sa. sociable_ideans t 1.n<->1:on t.id = u.tean_idU.tean_id = 1117 and sa.provider = "hubspot":* FROM activities WHERE uuid_to_bin('8024fffb-2df7-4017-91f4-d9f896850248') = uuid; # 79933459 YES* FROM activities WHERE UUid_to_Bin('[CREDIT_CARD]-9274-444da2a8185C') = Uuid; # 80186192 NO* FROM crn_configurations WHERE id = 1853;* FROM teans WHERE 1d = 1117:A console (EU?045 A1 441 X 66 ASresult = self::ánvokePrivateMethod('parseContactMobile', $service, [ScountryCode, ScreDatal):ounaeuncsion testpansetontacrtoonethlaidd, unheroe vordSservice = Sthis->getServiceMockO:ScrmData = ('MobilePhone' => *[PHONE]*]:Sresult = self::invokePrivateMethod('parseContactMobite', $service, [null, Sernbatal);Sthis->assertNotNull(Sresult);public function testParseContactMobiLeIsTruncatedTo25Chars(): voidSemce - schoocrservecchockSlongNumber = •[PHONE]345678991234*÷ScrmData = ['MobiLePhone' => $LongNumber);Sresult = self::invokePrivateMethod('parseContactMobile', $service, (null, ScrnDatal);thts-sassertNorkultSresulsthis-rassert lessthanbrequaluo, strien sresultspublic function testParseContactMobileWlithCountryCode): voidSservice = Sthis->oetSeryscelockiorSresult = self::invokePrivateMethod('parseContactMobile', Sservice, ['GB*, ScrnDatal):Sthis->assertstringStartslithehAccept Fle X-7117127337161000000 11000ОО 10008Д 1В 010П11)* FROM CMM.field_data fdcrn_ficlds f ON fd.crm field_id = f.ioactivities a ON fd.activity_id = a.idictivity_id = 79933459f.crn_provider_id = 'hs_activity_type':* FROM activity_messages:* fron text_relays where created_at > '2026-85-01';* fron activities where user_id IN (7160, 18608) and created_at > '2026-05-22' order by id desc;* fron users where team_id = 1 and id IN (18688, 13934, 7168);* fron activities where user_id = 7160 order by id desc Linit 10;* fron accounts where tean_id = 1 and nane = 'ColumnS";* fron users where name Like 'XSubrax"; # 31854, 1117* fron teans where id = 1117;* fron actávity_searches where user_id = 31854;* fron activity_search_filters nhere activity_search_id IN (88882, 88902)CascadellesorcesariceSthis->handle0bjectDeletion(Stead, Scrabata):Command col.oo ce tvrens/skas/11s/crv/S0eSt rte/Strvicrtete/paster... HAD -truncated 124 1еnoск0):scontig = sthot-contetscX.cont1gurur/eseoclassdielotion) :Now let me see the existing test file to understand its structure:Command ed, git, hoadce /uers/turts/te/uny/5e.mt.%i/ mh/5stesforce/ServiceTest.php | head -100declare(strict_types=1);nanespace Tests\Unit\Services\Crm\Salesforce;wce uainte oono 2loaen Gye let tena Vosboay:Let me seo the rest of the tost ile, especinlly the helocr methodk and the now tocte sởdod lin the PòNow l have a fuli picture of the test file. The PR already adds tests for the empty/nulli cases of parseContactPhonethat ocis parsed, and when there's an extension. Let me also check the TestPrivatcMethod trait and the parsephonalternhans aikahdlodnehtwkconteyt. oarsep.anewumber.he.cer.tuncton.detintcondlo.oneioohinExplored helpers.php and searched 3 queriesNow I have everything I need. The PR's existing tests only cover the empty/null early-return paths of parseContactPhone and parseContactMobite.l need to add:arstionthc Phone within renl anone numonr ind wrtand withh chone numeer thathas an exens onparseContactMobile with a real mobile number, with truncation (>25 chars)1453 tokengAsk anything (XOL)" HodhAdhotv• OwodturaTaame7Xelhes2 4 spac...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85709
|
2937
|
15
|
2026-05-28T12:59:26.619046+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973166619_m2.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Built-in Preview
Chrome
Firefox
Safari
Sync Changes
Hide This Notification
Code changed:...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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.8374335,"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":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"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 'TextRelayServiceTest'","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 'TextRelayServiceTest'","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":"Built-in Preview","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":"Chrome","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":"Firefox","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":"Safari","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":"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}]...
|
-5399681331318289114
|
-8920231051912821824
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Built-in Preview
Chrome
Firefox
Safari
Sync Changes
Hide This Notification
Code changed:
rapstomViewCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-lecanyice pn.ne heloers.onptnelereobiecistraicon:©ActivityControlier.php• HttpIntegrationsnlinteractionsUeerRokObserver.onpo Teanupo Nemeuoni(© HubspotJournalPollingService.phga Jobsclass servicelesc excenos lescuaseauisteneis07 MailModels0 Notifications2457247public function testResolveContactAccountSyncsllhenNotFoundLocallyO: void...=usaoepublic static function resolveContactCountryCodeProviderO=arcavfvosewvers743%(C SocialAccountObserverTest.UserObserverTest.phpUserRoleObserverTest.phoRRER*toetaprousden nesowetontrcrinuntruande?roudenPolciesnuhideunctinn testresol vetontact ountrwiodeda ProvideraServices>EACtVIYDa ActivityProviders2494249524961 24972498array Scrnbata,Dhnncnuntnusysstsl?string Sconvertedcode,?string Sexpected): void (..1usogelealendanleonterencopublic static function parseContactPhoneProvider: arrayi...)2545=1→RulihorClose• leonno* @dataRnovider parseContactPhoneProviderMermob acts2548 Cpuotae tunczon testParseContactPhonekzthEnptyPhone (array Scrabata, Pstring ScountryCode, array Sexpte 7HelpersTi HibsootUsaotpublic static function parseContactMobileProviderO: array...D IntegrationAppTnll istenars7567Pipedriveh Salaslorca7504*odata?rovider oarsecontact.ooreproviden=72%1722— 728› D FieldsD OpportunityMatcherl OpportunitySyncStratel ProsoectSearchStratecim sancettaisClientTest.phoC DecorateActivityTest.o( DeleteObiocts TraitTest 2644aanhtntoasieron© GetActivitvFieldNameTPavloadBuilder [EMAIL]© QuervHandlerTest.oha© Querviterator Test.ohoDACA©QuervResuitsTests.oheauai e tueten testeratertertkes tertrfe yhoa erey crmost, iotrin cuntrcote, eterin epublic function testImport0pportunitySkipsilhenProfileNotFound(): void{...)Cass Hac"anvsytanded aytends Hastanypublic function wheredl...public function firstot...RFRRRHZZZEcSemcettestono@*SuncRatchRedicSenics@ RaseSeniiceTest oho(CacharCrmSanicehomTO0У L7Thu 28 May 15:59:26+0.A SF fiminny@localhost)console (PROD) x C Service.php# console fiaum= console (STAGING)00 €Ty: AutowINTERVAL 38 DAY.BY u.id, u.email, u.name, u.softphone numbenBY sns count DESC045 A1 A41 У 66 A* fron teans where id = 1" fron rolesNCATCu,id. CASE WHEN u.id = thouner_id THEN • (oaner)* ELSE ** END) AS user_idormen fo FRil soctal accnuntssapargTncoson t.ida u.tean jaU.tean_id = 1117 and sa.provider = "hubspot":*EROM actMtes WHEOF Wund to binl18a2tetth-2147-1817.0144-40480695926801= Musd: " 70033459 VES* FROM activities WHERE uuid_to_bin('[CREDIT_CARD]-927f-4f4da2a8185c') = uuid: # 80186192 NO*EROM eon confaurazons MHEPF 3d = 1953%* FROM teans WHERE 1d = 1117;* Eoon licene whene 3d= 30240-* fron playbooks where id = 5473;*Eoon nlavbonk catanontas whane 4d = 47787%* fron playbook categonies where playbook_id = 5473:*Eoon enn ffalde whond 1d = 450222-* fron cra field values where crn_field_id = 659242:* FROM crn_field_data fdcrnfields f ON fd.crm field id = f.ioactivities a ON fd.activity id =a.idactivity_id = 79933459f.crn_provider_id = 'hs activity_type'where created at > *2926-05-91*-activities where user_id TN (7169, 18688) and created at > 2926-85-22' order by id desce• team 1d = 1 and id TN (18688, 13934. 7169):*£ron activities where usen 1a = 7169 order bu jd desc 1ini+ 10:*Toon tasne whendae*foon Antáusty seanches whane usen $a = 71852.*foon antsusty seanch filtene shere actsusty eenoch Sa TM (RRRR) RROp2).CascaollesorcesariceSthis->handle0bjectDeletion(Stead, Scrabata):Command col.ote t5/uns/5erv/es/crv/splestorte/Servtcrtest.paster...HEAD -struncated 124 Lines>..scontiosnsthodt contactockcconf1gururitscorloctlfelation):Now iet me sec the existind tast ile to underatand ite cinactureCommand cd, git, heador gurrster te3ts/uny/5e m/ces/crm/Sales torce/ServiceTest.php | heod -100dec hrelstrict typsselnanespace Tests\Unit\Services\Crm|Salesforce:uco 1uaiate 1evcatas o EDope E Re ot tons Vaskany:Let me seo the rest of the toat ile, especinlly the heloce methodk and the new tecte so dod in tha PoNow l have a fuli picture of the test file. The PR already adds tests for the empty/nulli cases of parseContactPhonethat ocis parsed, and when there's an extension. Let me also check the TestPrivatcMethod trait and the parsephonalternhans aikahdlodnetwstconteyt.oarsephonewumber.he.cer.tuncton.detiotcomnd.c.oneictcotinNow I have everything I need. The PR's existing tests only cover the empty/null early-return paths of parseContaaphooeand marsetontactMob/ile.llneed to.addarstvonthc Phone with nrenl ohone numonr ino wut and witha chone numoer tha thas an eeensionparseContactMobile with a real mobile number, with truncation (>25 chars)Servicetest.ohd$1322 tokengAsk anything (XoL)" HodhAdhotv• OKwweunamet4 spag...
|
85707
|
NULL
|
NULL
|
NULL
|
|
85708
|
2936
|
8
|
2026-05-28T12:59:19.652006+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973159652_m1.jpg...
|
PhpStorm
|
faVsco.js – ServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2• • 0ShellEditViewSessionScripts|ProfilesW iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKER881DEV (docker)₴82-zshN3screenpipe"app/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php84348-zsh+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec -itdocker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|X5ec2-user@ip-10-30-129-...100% <78• Thu 28 May 15:59:191₴1ec2-user@ip-10-30-140-...₴7...
|
NULL
|
-6747715845527353238
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2• • 0ShellEditViewSessionScripts|ProfilesW iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKER881DEV (docker)₴82-zshN3screenpipe"app/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php84348-zsh+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec -itdocker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|X5ec2-user@ip-10-30-129-...100% <78• Thu 28 May 15:59:191₴1ec2-user@ip-10-30-140-...₴7...
|
85703
|
NULL
|
NULL
|
NULL
|
|
85706
|
2937
|
13
|
2026-05-28T12:59:13.424178+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973153424_m2.jpg...
|
PhpStorm
|
faVsco.js – Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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.8374335,"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}]...
|
8569994499127135030
|
-3986301007428073024
|
idle
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
rapstomViewNeweNNCCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-lProinet v•) Service.php x php heipers.phg© Querylterator.phpу queуkesui onoservice one©) SyncBatchRedisService.ofd Traitsooaseeenone© BaseService.phpoCachecimsenroo© CountryCodeResolver.pnpC) CrmActivityProviderintegrateC CrmActivitvService.ohoccrmcont curationsettinos.ser© CrmObiectsResoiver.phpC) DeraultProsoec SearchStrateEmailHelper.php© FindsProspectinterface.php© LayoutManager.php© MatchDomainByEmailnterfac©OpportunityActivityMatcher.genoorur wsvoesttatkouote(@ OnnortunituSvnsStrataovBaeC [EMAIL](c ProspectSearchStrategyFact® ProspectSearchStrategyinter(© ProviderRegistry.phpd PocordColoator nhoResolveCompanyNameByEm:© TimePerioditerator.php@ impori@b internalUsciko cooscrytrionyTeom.oneo Nemeuoninanespace Jaminny servaces urn salestorce› use .8B8832258Ca Maiii>D Actions>D Office> E Repositories.>M Resoivers>E Traite>M Validators.© BatchCriteria.ohe©BatchCriterisInterface.oho©.BatchService.php8 BatchServiceinterace ono©[EMAIL] MnChsond Conhto nhn111119class service excends Baseservice Inplemencswles torceinertideSalesforceBatchSynCInterface,SunctontmerelignterthchSunctontrorer arecord woes ncerthcImportsBusinessProcesses.nterfaceRemoteEntityManipulationInterfaceFetchRelatedActivity.nterface,SendSumnaryToCrnInterfaceMatchDomainByEmailInterface,SearchTaskInterfaceLayoutManagementInterfaceSettingsInterface.Matchinintr des ntertaceSupports0b:lectTypeParseInterfaceRemoteNoteEntityManipulationInterfaceVerifyTaskExistsInterfaceuse ResolveCompanyNaneByEnailTraituse syncrelostraseuse Delete0biectsTraituse RecordManipulationsTrait:use ServiceTraits\BatchSyncTraituse FoLlowupac.v.cylraze* Note Body Liait for the Old Note-Toking Too*Avar snt1 usageonivate const int GLASSTC NOTE MAX LENGTH = 32600.*Note Pontent Himit fon the New Notes*Avan snt1 usagdprivate const int ENHANCED NOTE MAX LENGTH = 58000800:TO0У L7Thu 28 May 15:59:13+0.©ActivityControlier.phpA SF fiminny@localhost)© Client.phpa| 5A console [STAGING)D0.000as aua a an - aTy: AutowINTERVAL 38 DAY.BY u.id, u.email, u.name, u.softphone numbenBY sns_count DESC;* fron teans where id = 1;console (PROD) x C Service.php# console fiaum045 A1 A41 У 66 A714715-718719720BEERRBREBERRERTYNCAT(u.id, CASE WHEN u.id = t.ouner_id THEN • (ouner)* ELSE ** END) AS user_idouner_id FROM social_accounts saeans t 1.nc->1ontd n.teandU.tean_id = 1117 and sa.provider = "hubspot":* FROM activities WHERE uuid_to_bin(*8824fffb-2df7-4817-91f4-d9f896850248') = uuid; # 79933459 YES* FROM activities WHERE Uuid_to_bin('[CREDIT_CARD]-927f-4f4da2a8185c') = uuid; # 80186192 NO*EROM eon confaurazons MHEPF 3d = 1953%* FROM teans WHERE 1d = 1117;* Eoon licene whene 3d= 30240-* fron playbooks where id = 5473;* fron playbook categonies where id = 43783:* fron playbook categonies where playbook_id = 5473:*foon enn ffalde whond id = 450222.* fron cra_field values where cra_field_id = 659242:* FROM crn_field_data fdcrnfields f ON fd.crm field id = f.ioactivities a ON fd.activity_id = a.idactivity_id = 79933459f.crn_provider_id = 'hs activity_type'* FROM activity-messages:* fron text. relayswhere created at > *2926-05-91*-* fron actsivities where user_id TN (7168, 18688) and created at > *2826-85-22' order by sid descteam.1d = 1 and id TN (18688, 13934. 7168)=*fron activities where usen 1a = 7169 onder bu ja desc 13nit 18.*fnon accounts where tean 3a = 1and nane = "Colunos,.*Toon tasne whendae*foon Antáusty seanches whane usen $a = 71852.*Eoon sctkusty coanch Eilitane nhans actkusty canoch fA TM CARRR2 RRO02TAGte naetAn7erCascaollesorcesariceSthis->handle0bjectDeletion(Stead, Scrabata):Command col.ote t5/uns/5erv/es/crv/splestorte/Servtcrtest.paster...HEAD -struncated 124 Lines>..scontiosnsthodt contactockcconf1gururitscorloctlfelation):Now iet me sec the existind teet iie to underetand ite cinychureCommand cd, git, heador gurrster te3ts/uny/5e m/ces/crm/Sales torce/ServiceTest.php | heod -100dec hrelstrict typsselnanespace Tests\Unit\Services\Crm|Salesforce:uco 1uatate 1evcabas oLE ogen E Re Lot tons Vaskany:Let me seo the rest of the tost ile, especinlly the helocr methodk and the now tocte sởdod lin the PòNow l have a fuli picture of the test file. The PR already adds tests for the empty/nulli cases of parseContactPhonethat ocis parsed, and when there's an extension. Let me also check the TestPrivatcMethod trait and the parsephonalternhans aikahdlodnetwstconteyt.oarsephonewumber.he.cer.tuncton.detiotcomnd.c.oneictcotinSxolozad balbere nbn and casrcbad 2 auariaeNow I have everything I need. The PR's existing tests only cover the empty/null early-return paths of parseContaaphooeand marsetontactMob/ile.llneed to.addarstionthc Phone within renl anone numonr ind wrtand withh chone numeer thathas an exens onparseContactMobile with a real mobile number, with truncation (>25 chars)184 tokengAsk anything (XoL)Adhotv• OXtModtur Tatme Toxcsehireht4 spad...
|
85704
|
NULL
|
NULL
|
NULL
|
|
85705
|
2936
|
7
|
2026-05-28T12:59:10.544306+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973150544_m1.jpg...
|
PhpStorm
|
faVsco.js – 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
|
idle
|
hybrid
|
NULL
|
Project: faVsco.js, menu
iTerm2• • 0ShellEditVie Project: faVsco.js, menu
iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKERO ₴1DEV (docker)₴82-zshN3screenpipe"O ₴4-zshapp/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php348+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec-it docker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|85ec2-user@ip-10-30-129-...100% <78• Thu 28 May 15:59:101₴1ec2-user@ip-10-30-140-...₴7...
|
85703
|
NULL
|
NULL
|
NULL
|
|
85704
|
2937
|
12
|
2026-05-28T12:58:40.895762+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973120895_m2.jpg...
|
PhpStorm
|
faVsco.js – Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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.8374335,"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":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"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 'TextRelayServiceTest'","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 'TextRelayServiceTest'","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}]...
|
-5599254006743683834
|
-7050965352302034496
|
idle
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
rapstomViewNeweNNCCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-lProinet v© Querylterator.phpу queуkesui onoservice one©) SyncBatchRedisService.ofd Traitsooaseeenone© BaseService.phpoCachecimsenroo© CountryCodeResolver.pnpC) CrmActivityProviderintegrateC CrmActivitvService.ohoccrmcont curationsettinos.ser© CrmObiectsResoiver.phpC) DeraultProsoec SearchStrateEmailHelper.php© FindsProspectinterface.php© LayoutManager.php8 MatchDomainByEmailinterfac©OpportunityActivityMatcher.genoorur wsvoesttatkouote@ OnnorturitvSvoeStrtaosBaeC [EMAIL](c ProspectSearchStrategyFact® ProspectSearchStrategyinter(© ProviderRegistry.phpd PocordColoator nhoResolveCompanyNameByEm:© TimePerioditerator.php@ impori@b internalCa Maiii>D Actions>D Office> E Repositories.>M Resoivers>E Traite>M Validators.©) BatchCriteria.ohd©BatchCriterisInterface.oho©.BatchService.php8 BatchServiceinterace ono©[EMAIL] MnChsond Conhto nhn•) Service.php x php heipers.phgUsciko cooscrytrionyTeom.oney kemetonsnanespace Jaminny servaces urn salestorce› use .88883225g169111119class service excends Baseservice Inplemencswles torceinertideSalesforceBatchSynCInterface,SunctontmerelignterthchSunctontrorer arecord woes ncerthcImportsBusinessProcesses.nterfaceRemoteEntityManipulationInterfaceFetchRelatedActivity.nterface,SendSumnaryToCrnInterfaceMatchDomainByEmailInterface,SearchTaskInterfaceLayoutManagementInterfaceSettingsInterface.MatchCrmEntitiesInterfaceSupports0b:lectTypeParseInterfaceRemoteNoteEntityManipulationinterfaceVerifyTaskExistsInterfaceuse ResolveCompanyNaneByEnailTraituse syncrelostraseuse Delete0biectsTraituse RecordManipulationsTrait:use ServiceTraits\BatchSyncTraituse FoLlowupac.v.cylraze* Note Body Liait for the Old Note-Toking Too*Avar snt1 usageonivate const int GLASSTC NOTE MAX LENGTH = 32600,*Note Pontent Hmit fon the New Notes*avan snt1 usagdprivate const int ENHANCED NOTE MAX LENGTH = 58000800:TO0У L7Thu 28 May 15:58:40+0.©ActivityControlier.phpA SF fiminny@localhost)© Client.phpa| 5A console [STAGING)D0.000as aua a an - aTy: AutowINTERVAL 38 DAY.BY u.id, u.email, u.name, u.softphone numbenBY sns_count DESC;* fron teans where id = 1;console (PROD) x C Service.php# console fiaum045 A1 A41 У 66 A714715-718719720BEERRBREBERRERTYNCAT(u.id, CASE WHEN u.id = t.ouner_id THEN • (ouner)' ELSE ** END) AS user_idouner_id FROM social_accounts saeans t 1.nc->1ontd n.teandU.tean_id = 1117 and sa.provider = "hubspot":* FROM activities WHERE uuid_to_bin(*8824fffb-2df7-4817-91f4-d9f896850248') = uuid; # 79933459 YES* FROM activities WHERE Uuid_to_bin('[CREDIT_CARD]-927f-4f4da2a8185c') = uuid; # 80186192 NO*EROM eon confaurazons MHEPF 3d = 1953%* FROM teans WHERE 1d = 1117;* Eoon ucene whena 3d = 30240.* fron playbooks where id = 5473;* fron playbook categonies where id = 43783:* fron playbook categonies where playbook_id = 5473:*foon enn ffalde whond id = 450222.* fron cra_field values where cra_field_id = 659242:* FROM crn_field_data fdcrnfields f ON fd.crm field id = f.ioactivities a ON fd.activity_id = a.idactivity_id = 79933459f.crn_provider_id = 'hs activity_type'* FROM activity-messages:* fron text. relayswhere created at > *2926-05-91*-* fron actsivities where user_id TN (7168, 18688) and created at > *2826-85-22' order by sid descteam.1d = 1 and id TN (18688, 13934. 7168)=*fron activities where usen 1a = 7169 onder bu ja desc 13nit 18.*fnon accounts where tean 3a = 1and nane = "Colunos*.*Toon tasne whendae*foon Antáusty seanches whane usen $a = 71852.*Eoon sctkusty coanch Silitane nhans actfusty canoch SA TM CARRR) RRO92 )Cascaollesorcesarice80 -1084,6 +1084,10 €a class Service extends BaseService inplement:this-shandleoblectbeletion Stcad, screbata:command colote tusuns/skas/dis/ry/sopestoe/e/Serviertest-phgaster...HEAD -enuncated24inessScontactsRelat ion-seethod ("update0rGreate"wudlllRetumn (Scontact)scontig = sthod+ contaetsc)x1t1gururn(scorlace)Re lotion):Nowler me see the existino tast ile to understand ite sunietureand cd, git, hea,•orsgusersteratests/uny/sseS& gitServices/om/Salestorce/Seryicerest,ohohesd -10cronpdec hrettrewoseinanespace Tests\Unit\Services\Crm\Salesforce:I1 luminate\0atabase\Eloquent\Relations \HasManywemeshenros.o.nores.nle.csocc.vnend ocemetroonothnew.trolinhaphNow l have a fuli picture of the test file. The PR already adds tests for the empty/null cases of parseContactPhoneparsecontactMooite, but is missing the positive/hapoy-path tests = when there's an actual phonc numbethat gets parsed, and when there's an extension. Let me also check the TestPrivatcMethod trait and the parsePhoNow let me resd the complete "parsePhoneNumber" function and understand its usage context by examining some keyRasdlxersAukasfielooshcolnoo/Sar/cas/Crm/Wibtoot/kaodo.twtw/GwncCrmfothtrattoho.0.0%Ask anything (XOL)" HodhAdhotvTAGte naetAn7er• OXtModtur Tatme Toxcsehireht4 spag...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85703
|
2936
|
6
|
2026-05-28T12:58:38.910638+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973118910_m1.jpg...
|
PhpStorm
|
faVsco.js – 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
|
idle
|
hybrid
|
NULL
|
Project: faVsco.js, menu
iTerm2• • 0ShellEditVie Project: faVsco.js, menu
iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKER₴81DEV (docker)₴82-zshN3screenpipe"O ₴4-zshapp/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php348+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec-it docker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D‹>0 (|X5ec2-user@ip-10-30-129-...100% <78• Thu 28 May 15:58:381₴1ec2-user@ip-10-30-140-...₴7...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85702
|
2937
|
11
|
2026-05-28T12:58:07.810230+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973087810_m2.jpg...
|
PhpStorm
|
faVsco.js – Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rapstomViewNeweNNCCoocWindowFV faVsco.|s ~#12121 o rapstomViewNeweNNCCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-lproidet•) Service.php x php heipers.phg© Querylterator.phpу queуkesui onoservice one©) SyncBatchRedisService.ptd Traitsooaseeenone© BaseService.phpoCachecimsenroo© CountryCodeResoiver.prpC) CrmActivityProviderintegrateC CrmActivitvService.ohoccrmcont curationsettinos.ser© CrmObiectsResoiver.phpC) DeraultProsoec SearchStrateEmailHelper.php© FindsProspectinterface.php© LayoutManager.php8 MatchDomainByEmailinterfac©OpportunityActivityMatcher.genoorur wsvoesttatkouote@ OnnorturitvSvoeStrtaosBaeC [EMAIL](c ProspectSearchStrategyFact® ProspectSearchStrategyinter(© ProviderRegistry.phpd PocordColoator nhoResolveCompanyNameByEm:© TimePerioditerator.php@ impori@b internalUsciko cooscrytrionyTeom.oneo Nemeuoninanespace Jaminny servaces urn salestorce› use .8B8832258Ca Maiii>D Actions>D Office> E Repositories.>M Resoivers>O Trait:>M Validators.©) BatchCriteria.ohd©BatchCriterisInterface.oho©.BatchService.php8 BatchServiceinterace ono©[EMAIL] MnChsond Conhto nhn111119class service excends Baseservice Inplemencswles torceinertideSalesforceBatchSynCInterface,Sunc tontheeuis interthceSunctontrorer arecord woes ncerthcImportsBusinessProcesses.nterfaceRemoteEntityManipulationInterfaceFetchRelatedActivity.nterface,SendSummaryToCrminterfaceMatchDomainByEmailInterface,SearchTaskInterfaceLayoutManagementInterfaceSettingsInterface.Matchinintr des ntertaceSupports0b:lectTypeParseInterfaceRemoteNoteEntityManipulationInterfaceVonCutaclsysotcintonfanduse ResolveCompanyNaneByEnailTraituse syncrelostraseuse Delete0biectsTraituse RecordManipulationsTrait:use ServiceTraits\BatchSyncTraituse FoLlowupac.v.cylraze* Note Body Liait for the Old Note-Toking Too*Avar snt1 usageonivate const int GLASSTC NOTE MAX LENGTH = 32600,*Note Pontent Hmit fon the New Notes*avan snt1 usagdprivate const int ENHANCED NOTE MAX LENGTH = 58000800:inu co woy to.oo.+0.©ActivityControlier.phpA SF fiminny@localhost)© Client.phpa| 5A console [STAGING)D0.000as aua a an - aTy: AutowINTERVAL 38 DAY.BY u.id, u.email, u.name, u.softphone numbenBY sns_count DESC;* fron teans where id = 1;console (PROD) x C Service.php# console fiaum045 A1 A41 У 66 4714715-718719720BEERRBREBERRERTYNCAT(u.id, CASE WHEN u.id = t.ouner_id THEN • (ouner)' ELSE ** END) AS user_idouner_id FROM social_accounts saeans t 1.nc->1ontd n.teandU.tean_id = 1117 and sa.provider = "hubspot":* FROM activities WHERE uuid_to_bin(*8824fffb-2df7-4817-91f4-d9f896050248') = uuid; # 79933459 YES* FROM activities WHERE Uuid_to_bin('[CREDIT_CARD]-927f-4f4da2a8185c') = uuid; # 80186192 NO*EROM eon confaurazons MHEPF 3d = 1953%* FROM teans WHERE 1d = 1117;* Eoon licene whene 3d= 30240-* fron playbooks where id = 5473;* fron playbook categonies where id = 43783:* fron playbook categonies where playbook_id = 5473:*foon enn ffalde whond id = 450222.* fron cra_field values where cra_field_id = 659242:* FROM crn_field_data fdcrnfields f ON fd.crm field id = f.ioactivities a ON fd.activity_id = a.idactivity_id = 79933459f.crn_provider_id = 'hs activity_type'* FROM activity-messages:* fron text. relayswhere created at > *2926-05-91*-* fron actsivities where user_id TN (7168, 18688) and created at > 2926-85-22' orden by id desciteam.1d = 1 and id TN (18688, 13934. 7168)=*£ron activities where usen 1a = 7169 order bu ja desc 1ini+ 18:*fnon accounts where tean 3a = 1and nane = "Colunos*.*Toon tasne whendae*foon Antáusty seanches whane usen $a = 71852.*Eoon sctkusty coanch Eilitane nhans actkusty cancch fa TM CRRRR2 RROQ2Cascadioles orce PhontamntulCcrmonthtWonilaghonsLet me tirst look at the PR dit to understand the changcs• cd /Users/lukas/jiminny/app 66 git diff origin/master...HEAD -nane-onlyspp/tery/ces/com/SrlesPorcetServyce.proller. phptacte/linst/Corusrac/er/C.lnctarra/amrataet.phCommand cd, giaspp/servs/10xr//alesyorce/ Servitetpfp origin/easter.. HEAD -arapp/selvaces/crosatestorcerservice.prsthis-shandledo ectdelet ion Stcac, Screbara)nCommand col.ote ts/uns/5erv/ces/crv/splestoree/Servicrtest-paster.. HEAD -ctnuncated 124iness@contactsPelatton-seethod(tuedate0rGreate*_wvd11Petum (&contact)scontip- = ethod +contaetse)xlt1gururn(scorlacs.ieTation):Now iet me see the existino tost ile to underetnnd ite atnachurcCAmmand dAids hanor /gun/ester:s/.ts/uny/Se 4.es/ crm/Solesforce/ServiceTest. php 1 head -108erohnanespace Tests\Unit\Services\Crm\Salesforce:Database \eloquent (Relations (HasHany=AGe naetAn7EKAsk anything (XOL)" HodhAdhotv• OXtModtur Tatme Toxcsehireht4 spag...
|
NULL
|
4395158066031919589
|
NULL
|
visual_change
|
ocr
|
NULL
|
rapstomViewNeweNNCCoocWindowFV faVsco.|s ~#12121 o rapstomViewNeweNNCCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-lproidet•) Service.php x php heipers.phg© Querylterator.phpу queуkesui onoservice one©) SyncBatchRedisService.ptd Traitsooaseeenone© BaseService.phpoCachecimsenroo© CountryCodeResoiver.prpC) CrmActivityProviderintegrateC CrmActivitvService.ohoccrmcont curationsettinos.ser© CrmObiectsResoiver.phpC) DeraultProsoec SearchStrateEmailHelper.php© FindsProspectinterface.php© LayoutManager.php8 MatchDomainByEmailinterfac©OpportunityActivityMatcher.genoorur wsvoesttatkouote@ OnnorturitvSvoeStrtaosBaeC [EMAIL](c ProspectSearchStrategyFact® ProspectSearchStrategyinter(© ProviderRegistry.phpd PocordColoator nhoResolveCompanyNameByEm:© TimePerioditerator.php@ impori@b internalUsciko cooscrytrionyTeom.oneo Nemeuoninanespace Jaminny servaces urn salestorce› use .8B8832258Ca Maiii>D Actions>D Office> E Repositories.>M Resoivers>O Trait:>M Validators.©) BatchCriteria.ohd©BatchCriterisInterface.oho©.BatchService.php8 BatchServiceinterace ono©[EMAIL] MnChsond Conhto nhn111119class service excends Baseservice Inplemencswles torceinertideSalesforceBatchSynCInterface,Sunc tontheeuis interthceSunctontrorer arecord woes ncerthcImportsBusinessProcesses.nterfaceRemoteEntityManipulationInterfaceFetchRelatedActivity.nterface,SendSummaryToCrminterfaceMatchDomainByEmailInterface,SearchTaskInterfaceLayoutManagementInterfaceSettingsInterface.Matchinintr des ntertaceSupports0b:lectTypeParseInterfaceRemoteNoteEntityManipulationInterfaceVonCutaclsysotcintonfanduse ResolveCompanyNaneByEnailTraituse syncrelostraseuse Delete0biectsTraituse RecordManipulationsTrait:use ServiceTraits\BatchSyncTraituse FoLlowupac.v.cylraze* Note Body Liait for the Old Note-Toking Too*Avar snt1 usageonivate const int GLASSTC NOTE MAX LENGTH = 32600,*Note Pontent Hmit fon the New Notes*avan snt1 usagdprivate const int ENHANCED NOTE MAX LENGTH = 58000800:inu co woy to.oo.+0.©ActivityControlier.phpA SF fiminny@localhost)© Client.phpa| 5A console [STAGING)D0.000as aua a an - aTy: AutowINTERVAL 38 DAY.BY u.id, u.email, u.name, u.softphone numbenBY sns_count DESC;* fron teans where id = 1;console (PROD) x C Service.php# console fiaum045 A1 A41 У 66 4714715-718719720BEERRBREBERRERTYNCAT(u.id, CASE WHEN u.id = t.ouner_id THEN • (ouner)' ELSE ** END) AS user_idouner_id FROM social_accounts saeans t 1.nc->1ontd n.teandU.tean_id = 1117 and sa.provider = "hubspot":* FROM activities WHERE uuid_to_bin(*8824fffb-2df7-4817-91f4-d9f896050248') = uuid; # 79933459 YES* FROM activities WHERE Uuid_to_bin('[CREDIT_CARD]-927f-4f4da2a8185c') = uuid; # 80186192 NO*EROM eon confaurazons MHEPF 3d = 1953%* FROM teans WHERE 1d = 1117;* Eoon licene whene 3d= 30240-* fron playbooks where id = 5473;* fron playbook categonies where id = 43783:* fron playbook categonies where playbook_id = 5473:*foon enn ffalde whond id = 450222.* fron cra_field values where cra_field_id = 659242:* FROM crn_field_data fdcrnfields f ON fd.crm field id = f.ioactivities a ON fd.activity_id = a.idactivity_id = 79933459f.crn_provider_id = 'hs activity_type'* FROM activity-messages:* fron text. relayswhere created at > *2926-05-91*-* fron actsivities where user_id TN (7168, 18688) and created at > 2926-85-22' orden by id desciteam.1d = 1 and id TN (18688, 13934. 7168)=*£ron activities where usen 1a = 7169 order bu ja desc 1ini+ 18:*fnon accounts where tean 3a = 1and nane = "Colunos*.*Toon tasne whendae*foon Antáusty seanches whane usen $a = 71852.*Eoon sctkusty coanch Eilitane nhans actkusty cancch fa TM CRRRR2 RROQ2Cascadioles orce PhontamntulCcrmonthtWonilaghonsLet me tirst look at the PR dit to understand the changcs• cd /Users/lukas/jiminny/app 66 git diff origin/master...HEAD -nane-onlyspp/tery/ces/com/SrlesPorcetServyce.proller. phptacte/linst/Corusrac/er/C.lnctarra/amrataet.phCommand cd, giaspp/servs/10xr//alesyorce/ Servitetpfp origin/easter.. HEAD -arapp/selvaces/crosatestorcerservice.prsthis-shandledo ectdelet ion Stcac, Screbara)nCommand col.ote ts/uns/5erv/ces/crv/splestoree/Servicrtest-paster.. HEAD -ctnuncated 124iness@contactsPelatton-seethod(tuedate0rGreate*_wvd11Petum (&contact)scontip- = ethod +contaetse)xlt1gururn(scorlacs.ieTation):Now iet me see the existino tost ile to underetnnd ite atnachurcCAmmand dAids hanor /gun/ester:s/.ts/uny/Se 4.es/ crm/Solesforce/ServiceTest. php 1 head -108erohnanespace Tests\Unit\Services\Crm\Salesforce:Database \eloquent (Relations (HasHany=AGe naetAn7EKAsk anything (XOL)" HodhAdhotv• OXtModtur Tatme Toxcsehireht4 spag...
|
85700
|
NULL
|
NULL
|
NULL
|
|
85701
|
2936
|
5
|
2026-05-28T12:58:04.853161+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973084853_m1.jpg...
|
PhpStorm
|
faVsco.js – Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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}]...
|
8569994499127135030
|
-3986301007428073024
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKERO ₴1DEV (docker)₴82-zshN3screenpipe"O ₴4-zshapp/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php348+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec-it docker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D< →0 lahlX5ec2-user@ip-10-30-129-...100% <78• Thu 28 May 15:58:041₴1ec2-user@ip-10-30-140-...₴7...
|
85697
|
NULL
|
NULL
|
NULL
|
|
85700
|
2937
|
10
|
2026-05-28T12:58:04.753320+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973084753_m2.jpg...
|
PhpStorm
|
faVsco.js – Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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.8374335,"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}]...
|
8569994499127135030
|
-3986301007428073024
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
rapstomViewNeweNNCCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-lProinet v•) Service.php x php heipers.phg© Querylterator.phpу queуkesui onoservice one©) SyncBatchRedisService.ofd Traitsooaseeenone© BaseService.phpoCachecimsenroo© CountryCodeResoiver.prpC) CrmActivityProviderintegrateC CrmActivitvService.ohocermeont ourationsettinos.ver© CrmObiectsResoiver.phpC) DeraultProsoec SearchStrateEmailHelper.phpFindsProspectinterface.php© LayoutManager.php© MatchDomainByEmailnterfac©OpportunityActivityMatcher.genoorur wsvoesttatkouote@ OnnorturitvSvoeStrtaosBaeC [EMAIL](c ProspectSearchStrategyFact® ProspectSearchStrategyinter(c ProviderRegistry.phpd PocordColoator nhoResolveCompanyNameByEm:© TimePerioditerator.php@ impori@b internalUsciko cooscrytrionyTeom.oney kemetonsnanespace Jaminny servaces urn salestorce› use .888832258Ca Maiii>D Actions>D Office> E Repositories.>M Resoivers>E Traite>M Validators.©) BatchCriteria.ohd©BatchCriterisInterface.oho©.BatchService.php8 BatchServiceinterace ono©[EMAIL] MnChsond Conhto nhn111119class service excends Baseservice Inplemencswles torceinertideSalesforceBatchSynCInterface,SunctontmerelignterthchSunctontrorer arecord woes ncerthcImportsBusinessProcesses.nterfaceRemoteEntityManipulationInterfaceFetchRelatedActivity.nterface,SendSummaryToCrminterfaceMatchDomainByEmailInterface,SearchTaskInterfaceLayoutManagementInterfaceSettingsInterface.Matchinintr des ntertaceSupports0b:lectTypeParseInterfaceRemoteNoteEntityManipulationInterfaceVonCutaclsysotcintonfanduse ResolveCompanyNaneByEnailTraituse syncrelostraseuse Delete0biectsTraituse kecororanzoulaczonstrazcuse ServiceTraits\BatchSyncTraituse FoLlowupac.v.cylraze* Note Body Liait for the Old Note-Toking Too*Avar snt1 usageonivate const int GLASSTC NOTE MAX LENGTH = 32600.*Note Pontent Hmit fon the New Notes*avan snt1 usagdprivate const int ENHANCED NOTE MAX LENGTH = 58000800:TO0У L7Thu 28 May 15:58:04+0.©ActivityControlier.phpA SF fiminny@localhost)console (PROD) x C Service.php# console fiaum© Client.phpa| 5A console [STAGING)D0.000as aua a an - aTy: Autow714715-718719720BEERRBREBERRERTYINTERVAL 38 DAY.BY u.id, u.email, u.name, u.softphone numbenBY sns_count DESC;045 A1 A41 У 66 4* fron teans where id = 1;NCAT(u.id, CASE WHEN u.id = t.ouner_id THEN • (ouner)* ELSE ** END) AS user_idouner_id FROM social_accounts saeans t 1.nc->1ontd n.teandU.tean_id = 1117 and sa.provider = "hubspot":* FROM activities WHERE uuid_to_bin(*8824fffb-2df7-4817-91f4-d9f896050248') = uuid; # 79933459 YES* FROM activities WHERE Uuid_to_bin('[CREDIT_CARD]-927f-444da2a8185c') = uuid; # 80186192 NO*EROM eon confaurazons MHEPF 3d = 1953%* FROM teans WHERE 1d = 1117;* Eoon ucene whena 3d = 30240.* fron playbooks where id = 5473;* fron playbook categonies where id = 43783:* fron playbook categonies where playbook_id = 5473:*Eoon enn flalde whons id = 45022.* fron cra_field values where cra_field_id = 659242:* FROM crn_field_data fdcrnfields f ON fd.crm field id = f.ioactivities a ON fd.activity_id = a.idactivity_id = 79933459f.crn_provider_id = 'hs activity_type'* FROM activity-messages:* fron text. relayswhere created at > *2926-05-91*-* fron actsivities where user_id TN (7168, 18688) and created at > 2926-85-22' orden by id desciteam.1d = 1 and id TN (18688, 13934. 7168)=*£ron activities where usen 1a = 7169 order bu ja desc 1ini+ 18:*fnon accounts where tean 3a = 1and nane = "Colunos*.*Toon tasne whendae*foon Antéusty seanches whene usen $a = 71852.*Eoon sctkusty coanch Eilitane nhans actkusty cancch fa TM CRRRR2 RROQ2Cascaooles orce Phontreturn (C, null);:(l'ext)) 7 nui : Strelimit(SparsedNumber ('exr), 10, *)niedoprivate function parseContactMoblie(?sting &countryCod, artay Scrmosta); aneit (empty(ScrmData(*MobilePhone"]))emeesobok ache Phoro unoeansine chandes• cd /Users/lukas/Siminny/app 6& git diff origin/master...HEAD —-name-ont'app/ser/cestcom/SriesPorectServyCe.eroller-phptwtehindt/Carufcas/Cra/Salestorce/Servicetest.ohmand cd, oihco/servs/ts/as//5atesy/rce/Servite.pp origtn/saster...HEAD -Sthis=>handle0bjectDeletion(Slead, Scrmbata):test5/uns/5erv/ces/crv/s8lestorte/Servicetest/paster... HEAD -etruncated 124 lines:Now let ma sea the axistine tast tle to undarstaod ite stnichuremand cd, git, heaorgunerssturisests/unyt/Servaces/rm/Salestorce/ServiceTest.php || head -100=TeGte nactan7lekAsk anything (XOL)AdhotvRun se+ (Skip• OXtModtur Tatme Toxcsehireht4 spad...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85699
|
2937
|
9
|
2026-05-28T12:57:54.158282+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973074158_m2.jpg...
|
PhpStorm
|
faVsco.js – Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
246
3
21
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Salesforce;
use Carbon\Carbon;
use Exception;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Jiminny\Component\Country\CountriesMap;
use Jiminny\Contracts\Acl\PermissionEnum;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\ImportsBusinessProcessesInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\SalesforceBatchSyncInterface;
use Jiminny\Contracts\Services\Crm\Provider\SalesforceInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\RemoteNoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SearchTaskInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmProfileRecordTypesInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Enums\CrmObject;
use Jiminny\Events\Activities\Crm\LeadConverted;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpBadRequestException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\NoResultsException;
use Jiminny\Exceptions\ServiceUnavailableException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Jobs\Crm\MatchActivitiesToNewOpportunity;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\ContactRole;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Crm\RecordType;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\TeamSettings;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\Crm\RecordTypeFieldValuesRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Helpers\ArrayIterator;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Services\Crm\Salesforce\Fields\FieldHelper;
use Jiminny\Services\Crm\Salesforce\Fields\FieldTypeConverter;
use Jiminny\Services\Crm\Salesforce\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\FollowupActivityTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\LogActivityTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\RecordManipulationsTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\SyncFieldsTrait;
use Jiminny\Utils\CurrencyFormatter;
use Jiminny\Utils\StringUtil;
use Ramsey\Uuid\Uuid;
use Sentry\Laravel\Facade as Sentry;
class Service extends BaseService implements
SalesforceInterface,
SalesforceBatchSyncInterface,
SyncCrmEntitiesInterface,
SyncCrmProfileRecordTypesInterface,
ImportsBusinessProcessesInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SearchTaskInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
SupportsObjectTypeParseInterface,
RemoteNoteEntityManipulationInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncFieldsTrait;
use DeleteObjectsTrait;
use RecordManipulationsTrait;
use ServiceTraits\BatchSyncTrait;
use FollowupActivityTrait;
use LogActivityTrait;
/**
* Note Body Limit for the Old Note-Taking Tool
*
* @var int
*/
private const int CLASSIC_NOTE_MAX_LENGTH = 32000;
/**
* Note Content Limit for the New Notes
*
* @var int
*/
private const int ENHANCED_NOTE_MAX_LENGTH = 50000000;
private const string INSTALLED_PACKAGE_ID = '033Tw0000007bKbIAI';
private const int CACHE_TTL = 600;
private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day - 86400
/**
* @var Client
*/
protected $client;
protected PayloadBuilder $payloadBuilder;
protected QueryHandler $queryHandler;
private OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
public function __construct(
Client $client,
PayloadBuilder $payloadBuilder,
protected Dispatcher $eventDispatcher,
private readonly CountriesMap $countriesMap,
private readonly ProspectPhotoPathService $prospectPhotoPathService,
) {
parent::__construct();
$this->client = $client;
$this->payloadBuilder = $payloadBuilder;
$this->queryHandler = app(QueryHandler::class, [
'client' => $this->client,
'logger' => $this->logger,
]);
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
}
public function getDisplayName(): string
{
return 'Salesforce';
}
public function getJobDelay(): int
{
return 1;
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
return $user->getSocialAccount(SocialAccount::PROVIDER_SALESFORCE);
}
public function verifyTaskExists(Activity $activity): bool
{
$crmProviderId = $activity->getCrmProviderId();
$cacheKey = "crm_task_exists:{$this->config->getId()}:$crmProviderId";
return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($activity, $crmProviderId) {
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
$this->logger->warning('[Salesforce] Cannot verify task - no playbook found', [
'activity' => $activity->getId(),
'crm_provider_id' => $crmProviderId,
]);
return false;
}
$objectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_EVENT ? 'Event' : 'Task';
try {
$record = $this->getRecord($objectType, $crmProviderId, ['Id', 'IsDeleted']);
return ! empty($record) && ($record['IsDeleted'] ?? false) === false;
} catch (HttpNotFoundException|HttpBadRequestException) {
$this->logger->info('[Salesforce] Activity record not found during verification', [
'activity' => $activity->getId(),
'object_type' => $objectType,
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return false;
}
});
}
public function query(string $queryToRun, array $parameters = []): QueryIterator
{
// Due to poorly designed external calls, this method cannot be entirely removed
return $this->queryHandler->query($queryToRun, $parameters);
}
/*=========== Organization Information ===============*/
/**
* Get a list of all the API Versions for the instance.
*
* @throws CrmException
*
* @return mixed
*
*/
public function getApiVersions()
{
$url = $this->config->crm_base_url . '/services/data';
$response = $this->client->get($url);
return json_decode($response->getBody(), true);
}
/**
* Gets the valid recordTypes for a given Salesforce Object via the describe API.
*/
private function getRecordTypes(string $crmObject): array
{
$url = $this->client->getObjectsUrl() . $crmObject . '/describe';
$response = $this->client->get($url);
$jsonResponse = json_decode($response->getBody(), true);
$fields = [];
foreach ($jsonResponse['recordTypeInfos'] as $row) {
$fields[] = ['recordTypeId' => $row['recordTypeId'], 'default' => $row['defaultRecordTypeMapping']];
}
return $fields;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize($fieldType, $fieldValue, $internal);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
$defaultFields = ($activityType === Playbook::ACTIVITY_TYPE_TASK)
? FieldDefinitions::defaultTaskFields()
: FieldDefinitions::defaultEventFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
// Setup the activity field as the default Type.
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'Type',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK, Playbook::ACTIVITY_TYPE_EVENT];
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldFilter = ($activityType === Playbook::ACTIVITY_TYPE_TASK)
? FieldDefinitions::taskFollowupFieldsFilter()
: FieldDefinitions::eventFollowupFieldsFilter();
foreach ($fieldFilter as $eachFilter) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $eachFilter);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
/**
* This one is now called only when ImportActivityTypes is triggered or SyncFieldMetadata executed manually
* Regular sync now uses SharedSyncFieldsTrait -> syncSingleObjectType
* Needs to be replaced later on
*/
public function syncField(Field $field): void
{
try {
if (FieldHelper::isCustomField($field)) {
$query = '
SELECT
Id, Metadata, TableEnumOrId
FROM
CustomField
WHERE
DeveloperName = :fieldName
AND
TableEnumOrId = :fieldType
AND
NamespacePrefix = :namespacePrefix';
// We need to constrain the field lookup to the object, in case it's used in multiple places.
$objectType = \in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true)
? 'activity'
: $field->object_type;
$sfFields = $this->queryHandler->metadata($query, [
'fieldName' => substr($field->crm_provider_id, 0, -\strlen('__c')),
'fieldType' => ucfirst($objectType),
// This is used to ensure we only consider the field within the org, not installed packages.
'namespacePrefix' => 'null',
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
// Sync field metadata.
$metadata = $sfField['Metadata'];
$field->description = mb_strimwidth($metadata['description'] ?? '', 0, 191);
$field->label = mb_strimwidth($metadata['label'] ?? '', 0, Field::LABEL_MAX_LENGTH);
$field->type = $this->convertFieldType($metadata['type'], $field->getEntityName());
$field->is_mandatory = ($metadata['required'] === true);
$field->length = $metadata['length'];
$field->default_value = mb_strimwidth(trim($metadata['defaultValue'] ?? '', '"'), 0, 191);
$field->save();
} else {
$query = '
SELECT
Id, DataType, DeveloperName, Label, Length, Description
FROM
FieldDefinition
WHERE
DurableId = :entityName';
$entityName = $field->getEntityName();
$sfFields = $this->queryHandler->metadata($query, [
'entityName' => $entityName,
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
$convertedType = $this->convertFieldType($sfField['DataType'], $entityName);
$label = mb_strimwidth($sfField['Label'], 0, Field::LABEL_MAX_LENGTH);
if ($field->isBusinessType()) {
$label = 'Opportunity Type';
}
$field->description = mb_strimwidth($sfField['Description'], 0, Field::DESCRIPTION_MAX_LENGTH);
$field->label = $label;
$field->type = $convertedType;
$field->length = $sfField['Length'];
$field->save();
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
private function convertFieldType(string $from, ?string $entityName = null): string
{
$converter = new FieldTypeConverter();
return $converter->convert($from, $entityName);
}
/**
* @inheritdoc
*/
public function importPicklistValues(Field $field): array
{
$values = [];
$fieldValues = [];
try {
if (FieldHelper::isCustomField($field)) {
$query = '
SELECT
Id, Metadata, TableEnumOrId
FROM
CustomField
WHERE
DeveloperName = :fieldName
AND
TableEnumOrId = :fieldType
AND
NamespacePrefix = :namespacePrefix';
// We need to constrain the field lookup to the object, in case it's used in multiple places.
$objectType = \in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true) ?
'activity' : $field->object_type;
$sfFields = $this->queryHandler->metadata($query, [
'fieldName' => substr($field->crm_provider_id, 0, -\strlen('__c')),
'fieldType' => ucfirst($objectType),
// This is used to ensure we only consider the field within the org, not installed packages.
'namespacePrefix' => 'null',
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
$valueSet = $sfField['Metadata']['valueSet'];
if ($valueSet['valueSetName'] === null) {
// Local picklist values can be obtained easily.
$picklistValues = $valueSet['valueSetDefinition']['value'];
} else {
// But for some fields, we just get the Global Value Picklist pointer so need to do more work.
$picklistValues = $this->importGlobalValuePicklistValues($valueSet['valueSetName']);
}
// Import all active values.
foreach ($picklistValues as $i => $sfFieldValue) {
// Setup default value.
if ($sfFieldValue['default']) {
$field->update(['default_value' => $sfFieldValue['valueName']]);
}
// This comes through as null if active (lol).
if ($sfFieldValue['isActive'] !== false) {
$values[] = [
'value' => $sfFieldValue['valueName'],
'label' => $sfFieldValue['valueName'],
'sequence' => $i,
'is_default' => $sfFieldValue['default'],
];
}
}
} else {
$objectFields = $this->getObjectFields($field->object_type);
$fieldId = $field->crm_provider_id;
// Only work with our field of interest.
$objectField = array_filter($objectFields, function ($item) use ($fieldId) {
return $item['name'] === $fieldId;
});
$objectField = array_shift($objectField);
if (empty($objectField['picklistValues']) === false) {
foreach ($objectField['picklistValues'] as $i => $sfFieldValue) {
// Skip inactive values.
if ($sfFieldValue['active'] === false) {
continue;
}
// Setup default value.
if ($sfFieldValue['defaultValue']) {
$field->update(['default_value' => $sfFieldValue['value']]);
}
$values[] = [
'value' => $sfFieldValue['value'],
'label' => $sfFieldValue['label'],
'sequence' => $i,
'is_default' => $sfFieldValue['defaultValue'],
];
}
}
}
$fieldsToPurge = $field->values()->get()->pluck('value')->toArray();
foreach ($values as $value) {
$value['value'] = substr($value['value'] ?? '', 0, 255);
$fieldValues[] = $field->values()->updateOrCreate([
'value' => $value['value'],
], $value);
// Remove this value from the ones we are going to purge.
if (($key = array_search($value['value'], $fieldsToPurge, true)) !== false) {
unset($fieldsToPurge[$key]);
}
}
// Delete the old values that are no longer used.
// Get IDs of the values to be deleted
$valuesToDelete = $field->values()->whereIn('value', $fieldsToPurge);
$valuesToDeleteIds = $valuesToDelete->pluck('id');
if (! $valuesToDeleteIds->isEmpty()) {
$recordTypeFieldValuesRepository = app(RecordTypeFieldValuesRepository::class);
$recordTypeFieldValuesRepository->deleteForCrmFieldValueIds($valuesToDeleteIds->toArray());
// Now safely delete from crm_field_values
$valuesToDelete->delete();
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
return $fieldValues;
}
/**
* Gets values from Global Value Picklists.
*/
private function importGlobalValuePicklistValues(string $picklistName): array
{
$query = '
SELECT
Metadata
FROM
GlobalValueSet
WHERE
DeveloperName = :picklistName
LIMIT 1';
try {
$sfValues = $this->queryHandler->metadata($query, [
'picklistName' => $picklistName,
]);
// There is always 1 result at this point.
$sfValue = $sfValues->current();
return $sfValue['Metadata']['customValue'];
} catch (NoResultsException $noResultsException) {
// Nothing returned.
return [];
}
}
/**
* @inheritdoc
*/
public function syncProfileRecordTypes(): void
{
$objectTypes = [
'lead',
'account',
'contact',
'opportunity',
'task',
'event',
];
foreach ($objectTypes as $objectType) {
try {
$crmRecordTypes = $this->getRecordTypes(ucfirst($objectType));
foreach ($crmRecordTypes as $crmRecordType) {
// If the record type is default and not the Master type, set this.
if ($crmRecordType['default'] && $crmRecordType['recordTypeId'] !== '012000000000000AAA') {
$recordType = $this->config->recordTypes()
->where('crm_provider_id', $crmRecordType['recordTypeId'])
->first();
if ($recordType) {
$this->profile->{$objectType . '_record_type_id'} = $recordType->id;
}
}
}
} catch (HttpNotFoundException $exception) {
Log::error('No access to ' . $objectType . ' object, skipping...');
// XXX: should we log this fact somewhere?
continue;
}
}
if ($this->profile->isDirty()) {
$this->profile->save();
}
}
/**
* Gets business processes.
*/
public function importBusinessProcesses(): void
{
$query = '
SELECT
Id, IsActive, Name, TableEnumOrId
FROM
BusinessProcess
WHERE
TableEnumOrId IN (\'Lead\',\'Opportunity\')';
try {
$sfProcesses = $this->queryHandler->query($query);
// Upsert all processes for the team.
foreach ($sfProcesses as $sfProcess) {
/** @var BusinessProcess $businessProcess */
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $sfProcess['Id'],
], [
'team_id' => $this->team->id,
'name' => $sfProcess['Name'],
'type' => $sfProcess['TableEnumOrId'] === 'Lead' ? 'lead' : 'opportunity',
'is_selectable' => $sfProcess['IsActive'],
]);
$this->importBusinessProcessStages($businessProcess);
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* Gets business process stages.
*/
private function importBusinessProcessStages(BusinessProcess $businessProcess): void
{
$query = '
SELECT
Metadata
FROM
BusinessProcess
WHERE
Id = :processId';
try {
$stages = [];
$sfProcessStages = $this->queryHandler->metadata($query, [
'processId' => $businessProcess->crm_provider_id,
]);
// There is always 1 result at this point.
$sfProcessStage = $sfProcessStages->current();
// Upsert all processes for the team.
foreach ($sfProcessStage['Metadata']['values'] as $sfProcessStage) {
$sanitizedName = urldecode($sfProcessStage['valueName']); // Must decode: "%2C" becomes "," etc.
$stage = $businessProcess->crm->stages()
// This MUST match on label because this API doesn't use API Name.
->where('label', $sanitizedName)
->where('type', $businessProcess->type)
->where('is_selectable', 1)
->first();
if ($stage) {
$stages[] = $stage->id;
}
}
$businessProcess->stages()->sync($stages);
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* Gets record types.
*/
public function importRecordTypes(): void
{
$query = '
SELECT
Id, IsActive, Name, BusinessProcessId, SobjectType
FROM
RecordType';
try {
$sfRecordTypes = $this->queryHandler->query($query);
// Upsert all record types for the process.
foreach ($sfRecordTypes as $sfRecordType) {
$businessProcess = null;
if ($sfRecordType['BusinessProcessId']) {
$businessProcess = $this->config->businessProcesses()
->where('crm_provider_id', $sfRecordType['BusinessProcessId'])
->first();
}
/** @var RecordType $recordType */
$recordType = $this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $sfRecordType['Id'],
], [
'team_id' => $this->team->id,
'type' => mb_strtolower($sfRecordType['SobjectType']),
'name' => $sfRecordType['Name'],
'is_selectable' => $sfRecordType['IsActive'],
'business_process_id' => $businessProcess->id ?? null,
]);
$this->importRecordTypeFieldValues($recordType);
}
} catch (NoResultsException $noResultsException) {
// Do nothing.
}
}
/**
* Import record type - field value mappings. This only works for standard fields.
*/
private function importRecordTypeFieldValues(RecordType $recordType): void
{
try {
$query = '
SELECT
Metadata
FROM
RecordType
WHERE
Id = :recordTypeId';
$sfFields = $this->queryHandler->metadata($query, [
'recordTypeId' => $recordType->crm_provider_id,
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
// Sync field metadata.
$picklists = $sfField['Metadata']['picklistValues'];
foreach ($picklists as $picklist) {
$field = $this->config->fields()->where([
'type' => Field::TYPE_PICKLIST,
'object_type' => $recordType->type,
'crm_provider_id' => $picklist['picklist'],
])->first();
if ($field) {
$fieldValues = [];
foreach ($picklist['values'] as $value) {
// Must decode: "%2C" becomes "," etc.
$fieldValue = $field->values()
->where('value', urldecode($value['valueName']))
->first();
if ($fieldValue) {
$fieldValues[] = $fieldValue->id;
}
}
$recordType->fieldValues()->sync($fieldValues);
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$params = [];
$missingStage = null;
if ($types === null) {
$types = [Stage::TYPE_LEAD, Stage::TYPE_OPPORTUNITY];
}
foreach ($types as $type) {
if ($type === Stage::TYPE_LEAD) {
$query = '
SELECT
Id, ApiName, MasterLabel, SortOrder
FROM
LeadStatus';
} else {
$query = '
SELECT
Id, ApiName, MasterLabel, IsActive, SortOrder, DefaultProbability
FROM
OpportunityStage';
}
if ($missingStageName) {
$escapedStageName = ValueNormalizer::replaceQueryWithStringLiterals($missingStageName);
$query .= ' WHERE ApiName = :stageName';
$params = [
'stageName' => $escapedStageName,
];
}
try {
$sfStages = $this->queryHandler->query($query, $params);
} catch (NoResultsException $exception) {
$sfStages = [];
}
$missingStage = null;
// Upsert all stages for the team.
foreach ($sfStages as $sfStage) {
$selectable = true;
if (array_key_exists('IsActive', $sfStage)) {
$selectable = $sfStage['IsActive'];
}
$this->restoreAnyTrashedEntity($this->config->stages(), $sfStage['Id']);
$stage = $this->config->stages()->updateOrCreate([
'crm_provider_id' => $sfStage['Id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($sfStage['ApiName'], 0, 50),
'label' => mb_strimwidth($sfStage['MasterLabel'], 0, 191),
'type' => $type,
'sequence' => $sfStage['SortOrder'] ?? 0,
'is_selectable' => $selectable,
'probability' => $sfStage['DefaultProbability'] ?? null,
]);
if ($missingStageName && $missingStageName === $sfStage['ApiName']) {
$missingStage = $stage;
}
}
if ($missingStageName && $missingStage === null) {
// If they requested a stage that still doesn't exist, it must be inactive so lazy create it.
$missingStage = $this->config->stages()->create([
'crm_provider_id' => Uuid::uuid4(),
'team_id' => $this->team->id,
'name' => mb_strimwidth($missingStageName, 0, 50),
'label' => mb_strimwidth($missingStageName, 0, 191),
'type' => $type,
'sequence' => 0,
'is_selectable' => 0,
]);
}
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
$syncCount = 0;
$fields = $this->getAllFieldsAsArray('lead');
if (\in_array('Id', $fields, true) === false) {
return $syncCount;
}
$query = '
SELECT ' . rtrim(implode(',', $fields), ',') . '
FROM Lead
WHERE LastModifiedDate > :since
ORDER BY LastModifiedDate ASC';
try {
$sfLeads = $this->queryHandler->query($query, [
'since' => $since->format('Y-m-d\TH:i:s\Z'),
]);
foreach ($sfLeads as $sfLead) {
// Only sync if previously imported.
if ($this->hasLead($sfLead['Id'])) {
$this->importLead($sfLead);
$syncCount++;
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::LEAD);
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
$fields = $this->getAllFieldsAsArray('lead');
$sfLead = $this->getRecord('Lead', $crmId, $fields);
return $this->importLead($sfLead);
}
private function importLead($crmData): ?Lead
{
/** @var ?Stage $stage */
$stage = null;
if (isset($crmData['Status'])) {
// Get the current stage.
$stage = $this->config
->stages()
->where('name', $crmData['Status'])
->where('type', Stage::TYPE_LEAD)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_LEAD], $crmData['Status']);
}
}
// If we have no way of importing this, just return null :(
if ($stage === null) {
return null;
}
$countryCode = $crmData['CountryCode'] ?? null;
// Salesforce allows custom "countries" to be created. Disregard these.
if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {
$countryCode = null;
}
// If we have no country code, try to parse it from the country name.
if ($countryCode === null && empty($crmData['Country']) !== false) {
$countryCode = $this->convertCountryNameToCode($crmData['Country']);
}
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['Phone'] ?? '', 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
$mobilePhone = null;
if (empty($crmData['MobilePhone']) === false) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['MobilePhone'], 0, 25);
$mobilePhone = phone_e164($countryCode, $number);
}
$convertedDate = null;
$convertedAccount = null;
$convertedOpportunity = null;
$convertedContact = null;
if ($crmData['IsConverted'] == 'true') {
$convertedDate = $crmData['ConvertedDate'];
if (empty($crmData['ConvertedAccountId']) === false) {
$convertedAccount = $this->config
->accounts()
->where('crm_provider_id', $crmData['ConvertedAccountId'])
->first();
if ($convertedAccount === null) {
try {
$convertedAccount = $this->syncAccount($crmData['ConvertedAccountId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
if (empty($crmData['ConvertedOpportunityId']) === false) {
$convertedOpportunity = $this->config
->opportunities()
->where('crm_provider_id', $crmData['ConvertedOpportunityId'])
->first();
if ($convertedOpportunity === null) {
try {
$convertedOpportunity = $this->syncOpportunity($crmData['ConvertedOpportunityId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
if (empty($crmData['ConvertedContactId']) === false) {
$convertedContact = $this->team
->crm
->contacts()
->where('crm_provider_id', $crmData['ConvertedContactId'])
->first();
if ($convertedContact === null) {
try {
$convertedContact = $this->syncContact($crmData['ConvertedContactId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
}
if (empty($crmData['Company'])) {
$company = 'Unknown';
} else {
$company = mb_strimwidth($crmData['Company'], 0, 191);
}
$domain = null;
if (empty($crmData['Website']) === false) {
$domain = mb_strimwidth($crmData['Website'], 0, 191);
$domain = StringUtil::resolveDomain($domain);
}
$createdDate = null;
if (empty($crmData['CreatedDate']) === false) {
$createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');
}
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$data = [
'team_id' => $this->team->id,
'user_id' => $profile?->user_id,
'owner_id' => $crmData['OwnerId'] ?? '',
'company' => $company,
'domain' => $domain,
'name' => $crmData['Name'] ? mb_strimwidth($crmData['Name'], 0, 191) : '',
'title' => $crmData['Title'] ? mb_strimwidth($crmData['Title'], 0, 128) : null,
'email' => $crmData['Email'] ? mb_strimwidth($crmData['Email'], 0, 80) : null,
'phone' => $parsedNumber['phone'],
'ext' => $parsedNumber['ext'] ?? null,
'mobile_phone' => $mobilePhone,
'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(
crmConfiguration: $this->config,
crmProviderId: $crmData['Id'],
modelType: Lead::class,
fileName: $crmData['Id'],
avatarText: $crmData['Name']
),
'stage_id' => $stage->id,
'record_type_id' => null,
'converted_at' => $convertedDate,
'converted_account_id' => $convertedAccount->id ?? null,
'converted_opportunity_id' => $convertedOpportunity->id ?? null,
'converted_contact_id' => $convertedContact->id ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $createdDate,
];
$this->restoreAnyTrashedEntity($this->config->leads(), $crmData['Id']);
/** @var Lead $lead */
$lead = $this->config->leads()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);
if ($lead->wasChanged('converted_at') && $lead->getConvertedAt() !== null) {
$this->eventDispatcher->dispatch(new LeadConverted($lead));
}
$this->handleObjectDeletion($lead, $crmData);
if ($lead->trashed()) {
return null;
}
return $lead;
}
/**
* @inheritdoc
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
$fields = $this->getAllFieldsAsArray('account');
if (\in_array('Id', $fields, true) === false) {
return $syncCount;
}
$query = '
SELECT ' . rtrim(implode(',', $fields), ',') . '
FROM Account
WHERE LastModifiedDate > :since
ORDER BY LastModifiedDate ASC';
try {
$sfAccounts = $this->queryHandler->query($query, [
'since' => $since->format('Y-m-d\TH:i:s\Z'),
]);
foreach ($sfAccounts as $sfAccount) {
// Only sync if previously imported.
if ($this->hasAccount($sfAccount['Id'])) {
$this->importAccount($sfAccount);
$syncCount++;
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::ACCOUNT);
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
$fields = $this->getAllFieldsAsArray('account');
if (! in_array('Id', $fields, true)) {
$this->logger->info('[Salesforce] Sync account cancelled. Fields are not available.', [
'crmId' => $crmId,
'userId' => $this->profile->getUserId(),
]);
return null;
}
$sfAccount = $this->getRecord('Account', $crmId, $fields);
return $this->importAccount($sfAccount);
}
private function importAccount($crmData): ?Account
{
if (! empty($crmData['IsDeleted'])) {
$this->handleEntityDeletionByProviderId($this->config->accounts(), $crmData);
return null;
}
$countryCode = $crmData['BillingCountryCode'] ?? $crmData['ShippingCountryCode'] ?? null;
// Salesforce allows custom "countries" to be created. Disregard these.
if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {
$countryCode = null;
}
// If we have no country code, try to parse it from the country names.
if ($countryCode === null && empty($crmData['BillingCountry']) === false) {
$countryCode = $this->convertCountryNameToCode($crmData['BillingCountry']);
}
if ($countryCode === null && empty($crmData['ShippingCountry']) === false) {
$countryCode = $this->convertCountryNameToCode($crmData['ShippingCountry']);
}
if (empty($crmData['Phone']) === false) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['Phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$industry = null;
if (empty($crmData['Industry']) === false) {
$industry = mb_strimwidth($crmData['Industry'], 0, 40);
}
$domain = null;
if (empty($crmData['Website']) === false) {
$domain = mb_strimwidth($crmData['Website'], 0, 191);
$domain = StringUtil::resolveDomain($domain);
}
$createdDate = null;
if (empty($crmData['CreatedDate']) === false) {
$createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');
}
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$data = [
'team_id' => $this->team->id,
'user_id' => $profile?->user_id,
'owner_id' => $crmData['OwnerId'],
'name' => mb_strimwidth($crmData['Name'], 0, 191),
'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(
crmConfiguration: $this->config,
crmProviderId: $crmData['Id'],
modelType: Account::class,
fileName: $crmData['Id'],
avatarText: $crmData['Name']
),
'industry' => $industry,
'domain' => $domain,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $createdDate,
];
$this->restoreAnyTrashedEntity($this->config->accounts(), $crmData['Id']);
/** @var Account $account */
$account = $this->config->accounts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);
$this->handleObjectDeletion($account, $crmData);
return $account->trashed() ? null : $account;
}
/**
* @inheritdoc
*/
public function syncOpportunities(array $parameters, ?string $strategy = null): int
{
$strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);
$syncCount = 0;
$logParams = $parameters;
$parameters['profile'] = $this->profile;
$logParams['user'] = $this->profile->getUserId();
if (count($strategies) > 1) {
$this->logger->warning('[' . $this->getDisplayName() . '] Multiple sync strategies used', [
'teamId' => $this->team->getUuid(),
'params' => $logParams,
'strategies_count' => count($strategies),
]);
}
foreach ($strategies as $syncStrategy) {
$name = $syncStrategy->getStrategyName();
try {
$sfOpportunities = $syncStrategy->fetchOpportunities($parameters);
$totalRecords = $sfOpportunities->count();
foreach ($sfOpportunities as $sfOpportunity) {
$this->importOpportunity($sfOpportunity);
$syncCount++;
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
$this->logger->warning('[' . $this->getDisplayName() . '] No opportunities found', [
'teamId' => $this->team->getUuid(),
'name' => $name,
'params' => $logParams,
'reason' => $noResultsException->getMessage(),
]);
} catch (CrmException $crmException) {
// Nothing to sync.
$this->logger->warning('[' . $this->getDisplayName() . '] Opportunity sync failed', [
'teamId' => $this->team->getUuid(),
'name' => $name,
'params' => $logParams,
'reason' => $crmException->getMessage(),
]);
}
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::OPPORTUNITY, ['params' => $logParams]);
// debug to see how if count of opportunities reaches 1000
if ($syncCount >= 1000) {
$this->logger->info(
'[' . $this->getDisplayName() . '] Sync Opportunities - count warning',
[
'team_id' => $this->team->getId(),
'params' => $logParams,
'count' => $syncCount,
'strategies_count' => count($strategies),
'total_records' => $totalRecords ?? null,
]
);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncOpportunity(string $crmId): ?Opportunity
{
$strategy = $this->opportunitySyncStrategyResolver->resolve(
$this->config,
OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY
);
$parameters = [
'profile' => $this->profile,
'crm_id' => $crmId,
];
try {
$sfOpportunity = $strategy->fetchOpportunities($parameters);
} catch (HttpNotFoundException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [
'teamId' => $this->team->id_string,
'crmId' => $crmId,
]);
return null;
} catch (CrmException $crmException) {
$this->logger->info('[' . $this->getDisplayName() . '] Opportunity sync failed', [
'teamId' => $this->team->id_string,
'crmId' => $crmId,
'exception' => $crmException->getMessage(),
]);
return null;
}
if ($sfOpportunity instanceof ArrayIterator) {
return $this->importOpportunity($sfOpportunity->getItems());
}
return $this->importOpportunity($sfOpportunity);
}
/**
* @throws HttpNotFoundException
*/
private function importOpportunity($crmData): ?Opportunity
{
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$account = null;
if (empty($crmData['AccountId']) === false) {
/** @var ?Account $account */
$account = $this->config->accounts()
->where('crm_provider_id', (string) $crmData['AccountId'])
->first();
if ($account === null) {
$account = $this->syncAccount($crmData['AccountId']);
}
}
$userId = $profile?->getUserId() ?? $account?->getUserId();
if ($userId === null) {
$this->logger->error('[Salesforce] | Skip import, no user_id found', [
'id' => $crmData['Id'],
]);
return null;
}
/** @var ?Stage $stage */
$stage = null;
if (i...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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.8374335,"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":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"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 'TextRelayServiceTest'","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 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12","depth":4,"bounds":{"left":0.35272607,"top":0.12529927,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"246","depth":4,"bounds":{"left":0.3643617,"top":0.12529927,"width":0.012632979,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.37898937,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"21","depth":4,"bounds":{"left":0.38896278,"top":0.12529927,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40026596,"top":0.123703115,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40757978,"top":0.123703115,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Salesforce;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Events\\Dispatcher;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\Country\\CountriesMap;\nuse Jiminny\\Contracts\\Acl\\PermissionEnum;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ImportsBusinessProcessesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\SalesforceBatchSyncInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\SalesforceInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteNoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SearchTaskInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmProfileRecordTypesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Enums\\CrmObject;\nuse Jiminny\\Events\\Activities\\Crm\\LeadConverted;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpBadRequestException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\NoResultsException;\nuse Jiminny\\Exceptions\\ServiceUnavailableException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Jobs\\Crm\\MatchActivitiesToNewOpportunity;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\ContactRole;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Crm\\RecordType;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\TeamSettings;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\Crm\\RecordTypeFieldValuesRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Helpers\\ArrayIterator;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\FieldHelper;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\FieldTypeConverter;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\FollowupActivityTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\LogActivityTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\RecordManipulationsTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse Jiminny\\Utils\\StringUtil;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry\\Laravel\\Facade as Sentry;\n\nclass Service extends BaseService implements\n SalesforceInterface,\n SalesforceBatchSyncInterface,\n SyncCrmEntitiesInterface,\n SyncCrmProfileRecordTypesInterface,\n ImportsBusinessProcessesInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SearchTaskInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n SupportsObjectTypeParseInterface,\n RemoteNoteEntityManipulationInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncFieldsTrait;\n use DeleteObjectsTrait;\n use RecordManipulationsTrait;\n use ServiceTraits\\BatchSyncTrait;\n use FollowupActivityTrait;\n use LogActivityTrait;\n\n /**\n * Note Body Limit for the Old Note-Taking Tool\n *\n * @var int\n */\n private const int CLASSIC_NOTE_MAX_LENGTH = 32000;\n\n /**\n * Note Content Limit for the New Notes\n *\n * @var int\n */\n private const int ENHANCED_NOTE_MAX_LENGTH = 50000000;\n\n private const string INSTALLED_PACKAGE_ID = '033Tw0000007bKbIAI';\n\n private const int CACHE_TTL = 600;\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day - 86400\n\n /**\n * @var Client\n */\n protected $client;\n\n protected PayloadBuilder $payloadBuilder;\n protected QueryHandler $queryHandler;\n\n private OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n\n public function __construct(\n Client $client,\n PayloadBuilder $payloadBuilder,\n protected Dispatcher $eventDispatcher,\n private readonly CountriesMap $countriesMap,\n private readonly ProspectPhotoPathService $prospectPhotoPathService,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->payloadBuilder = $payloadBuilder;\n $this->queryHandler = app(QueryHandler::class, [\n 'client' => $this->client,\n 'logger' => $this->logger,\n ]);\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n }\n\n public function getDisplayName(): string\n {\n return 'Salesforce';\n }\n\n public function getJobDelay(): int\n {\n return 1;\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n return $user->getSocialAccount(SocialAccount::PROVIDER_SALESFORCE);\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($activity, $crmProviderId) {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n $this->logger->warning('[Salesforce] Cannot verify task - no playbook found', [\n 'activity' => $activity->getId(),\n 'crm_provider_id' => $crmProviderId,\n ]);\n\n return false;\n }\n\n $objectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_EVENT ? 'Event' : 'Task';\n\n try {\n $record = $this->getRecord($objectType, $crmProviderId, ['Id', 'IsDeleted']);\n\n return ! empty($record) && ($record['IsDeleted'] ?? false) === false;\n } catch (HttpNotFoundException|HttpBadRequestException) {\n $this->logger->info('[Salesforce] Activity record not found during verification', [\n 'activity' => $activity->getId(),\n 'object_type' => $objectType,\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n });\n }\n\n public function query(string $queryToRun, array $parameters = []): QueryIterator\n {\n // Due to poorly designed external calls, this method cannot be entirely removed\n return $this->queryHandler->query($queryToRun, $parameters);\n }\n\n /*=========== Organization Information ===============*/\n\n /**\n * Get a list of all the API Versions for the instance.\n *\n * @throws CrmException\n *\n * @return mixed\n *\n */\n public function getApiVersions()\n {\n $url = $this->config->crm_base_url . '/services/data';\n\n $response = $this->client->get($url);\n\n return json_decode($response->getBody(), true);\n }\n\n /**\n * Gets the valid recordTypes for a given Salesforce Object via the describe API.\n */\n private function getRecordTypes(string $crmObject): array\n {\n $url = $this->client->getObjectsUrl() . $crmObject . '/describe';\n\n $response = $this->client->get($url);\n $jsonResponse = json_decode($response->getBody(), true);\n\n $fields = [];\n foreach ($jsonResponse['recordTypeInfos'] as $row) {\n $fields[] = ['recordTypeId' => $row['recordTypeId'], 'default' => $row['defaultRecordTypeMapping']];\n }\n\n return $fields;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize($fieldType, $fieldValue, $internal);\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n $defaultFields = ($activityType === Playbook::ACTIVITY_TYPE_TASK)\n ? FieldDefinitions::defaultTaskFields()\n : FieldDefinitions::defaultEventFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n // Setup the activity field as the default Type.\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'Type',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK, Playbook::ACTIVITY_TYPE_EVENT];\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n\n $fieldFilter = ($activityType === Playbook::ACTIVITY_TYPE_TASK)\n ? FieldDefinitions::taskFollowupFieldsFilter()\n : FieldDefinitions::eventFollowupFieldsFilter();\n\n foreach ($fieldFilter as $eachFilter) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $eachFilter);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n /**\n * This one is now called only when ImportActivityTypes is triggered or SyncFieldMetadata executed manually\n * Regular sync now uses SharedSyncFieldsTrait -> syncSingleObjectType\n * Needs to be replaced later on\n */\n public function syncField(Field $field): void\n {\n try {\n if (FieldHelper::isCustomField($field)) {\n $query = '\n SELECT\n Id, Metadata, TableEnumOrId\n FROM\n CustomField\n WHERE\n DeveloperName = :fieldName\n AND\n TableEnumOrId = :fieldType\n AND\n NamespacePrefix = :namespacePrefix';\n\n // We need to constrain the field lookup to the object, in case it's used in multiple places.\n $objectType = \\in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true)\n ? 'activity'\n : $field->object_type;\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'fieldName' => substr($field->crm_provider_id, 0, -\\strlen('__c')),\n 'fieldType' => ucfirst($objectType),\n\n // This is used to ensure we only consider the field within the org, not installed packages.\n 'namespacePrefix' => 'null',\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Sync field metadata.\n $metadata = $sfField['Metadata'];\n\n $field->description = mb_strimwidth($metadata['description'] ?? '', 0, 191);\n $field->label = mb_strimwidth($metadata['label'] ?? '', 0, Field::LABEL_MAX_LENGTH);\n $field->type = $this->convertFieldType($metadata['type'], $field->getEntityName());\n $field->is_mandatory = ($metadata['required'] === true);\n $field->length = $metadata['length'];\n $field->default_value = mb_strimwidth(trim($metadata['defaultValue'] ?? '', '\"'), 0, 191);\n $field->save();\n } else {\n $query = '\n SELECT\n Id, DataType, DeveloperName, Label, Length, Description\n FROM\n FieldDefinition\n WHERE\n DurableId = :entityName';\n\n $entityName = $field->getEntityName();\n $sfFields = $this->queryHandler->metadata($query, [\n 'entityName' => $entityName,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n $convertedType = $this->convertFieldType($sfField['DataType'], $entityName);\n $label = mb_strimwidth($sfField['Label'], 0, Field::LABEL_MAX_LENGTH);\n\n if ($field->isBusinessType()) {\n $label = 'Opportunity Type';\n }\n\n $field->description = mb_strimwidth($sfField['Description'], 0, Field::DESCRIPTION_MAX_LENGTH);\n $field->label = $label;\n $field->type = $convertedType;\n $field->length = $sfField['Length'];\n $field->save();\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n private function convertFieldType(string $from, ?string $entityName = null): string\n {\n $converter = new FieldTypeConverter();\n\n return $converter->convert($from, $entityName);\n }\n\n /**\n * @inheritdoc\n */\n public function importPicklistValues(Field $field): array\n {\n $values = [];\n $fieldValues = [];\n\n try {\n if (FieldHelper::isCustomField($field)) {\n $query = '\n SELECT\n Id, Metadata, TableEnumOrId\n FROM\n CustomField\n WHERE\n DeveloperName = :fieldName\n AND\n TableEnumOrId = :fieldType\n AND\n NamespacePrefix = :namespacePrefix';\n\n // We need to constrain the field lookup to the object, in case it's used in multiple places.\n $objectType = \\in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true) ?\n 'activity' : $field->object_type;\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'fieldName' => substr($field->crm_provider_id, 0, -\\strlen('__c')),\n 'fieldType' => ucfirst($objectType),\n // This is used to ensure we only consider the field within the org, not installed packages.\n 'namespacePrefix' => 'null',\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n $valueSet = $sfField['Metadata']['valueSet'];\n\n if ($valueSet['valueSetName'] === null) {\n // Local picklist values can be obtained easily.\n $picklistValues = $valueSet['valueSetDefinition']['value'];\n } else {\n // But for some fields, we just get the Global Value Picklist pointer so need to do more work.\n $picklistValues = $this->importGlobalValuePicklistValues($valueSet['valueSetName']);\n }\n\n // Import all active values.\n foreach ($picklistValues as $i => $sfFieldValue) {\n // Setup default value.\n if ($sfFieldValue['default']) {\n $field->update(['default_value' => $sfFieldValue['valueName']]);\n }\n\n // This comes through as null if active (lol).\n if ($sfFieldValue['isActive'] !== false) {\n $values[] = [\n 'value' => $sfFieldValue['valueName'],\n 'label' => $sfFieldValue['valueName'],\n 'sequence' => $i,\n 'is_default' => $sfFieldValue['default'],\n ];\n }\n }\n } else {\n $objectFields = $this->getObjectFields($field->object_type);\n $fieldId = $field->crm_provider_id;\n\n // Only work with our field of interest.\n $objectField = array_filter($objectFields, function ($item) use ($fieldId) {\n return $item['name'] === $fieldId;\n });\n\n $objectField = array_shift($objectField);\n if (empty($objectField['picklistValues']) === false) {\n foreach ($objectField['picklistValues'] as $i => $sfFieldValue) {\n // Skip inactive values.\n if ($sfFieldValue['active'] === false) {\n continue;\n }\n\n // Setup default value.\n if ($sfFieldValue['defaultValue']) {\n $field->update(['default_value' => $sfFieldValue['value']]);\n }\n\n $values[] = [\n 'value' => $sfFieldValue['value'],\n 'label' => $sfFieldValue['label'],\n 'sequence' => $i,\n 'is_default' => $sfFieldValue['defaultValue'],\n ];\n }\n }\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'] ?? '', 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, true)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n // Get IDs of the values to be deleted\n $valuesToDelete = $field->values()->whereIn('value', $fieldsToPurge);\n $valuesToDeleteIds = $valuesToDelete->pluck('id');\n if (! $valuesToDeleteIds->isEmpty()) {\n $recordTypeFieldValuesRepository = app(RecordTypeFieldValuesRepository::class);\n $recordTypeFieldValuesRepository->deleteForCrmFieldValueIds($valuesToDeleteIds->toArray());\n\n // Now safely delete from crm_field_values\n $valuesToDelete->delete();\n }\n\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n return $fieldValues;\n }\n\n /**\n * Gets values from Global Value Picklists.\n */\n private function importGlobalValuePicklistValues(string $picklistName): array\n {\n $query = '\n SELECT\n Metadata\n FROM\n GlobalValueSet\n WHERE\n DeveloperName = :picklistName\n LIMIT 1';\n\n try {\n $sfValues = $this->queryHandler->metadata($query, [\n 'picklistName' => $picklistName,\n ]);\n\n // There is always 1 result at this point.\n $sfValue = $sfValues->current();\n\n return $sfValue['Metadata']['customValue'];\n } catch (NoResultsException $noResultsException) {\n // Nothing returned.\n\n return [];\n }\n }\n\n /**\n * @inheritdoc\n */\n public function syncProfileRecordTypes(): void\n {\n $objectTypes = [\n 'lead',\n 'account',\n 'contact',\n 'opportunity',\n 'task',\n 'event',\n ];\n\n foreach ($objectTypes as $objectType) {\n try {\n $crmRecordTypes = $this->getRecordTypes(ucfirst($objectType));\n\n foreach ($crmRecordTypes as $crmRecordType) {\n // If the record type is default and not the Master type, set this.\n if ($crmRecordType['default'] && $crmRecordType['recordTypeId'] !== '012000000000000AAA') {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $crmRecordType['recordTypeId'])\n ->first();\n\n if ($recordType) {\n $this->profile->{$objectType . '_record_type_id'} = $recordType->id;\n }\n }\n }\n } catch (HttpNotFoundException $exception) {\n Log::error('No access to ' . $objectType . ' object, skipping...');\n\n // XXX: should we log this fact somewhere?\n continue;\n }\n }\n\n if ($this->profile->isDirty()) {\n $this->profile->save();\n }\n }\n\n /**\n * Gets business processes.\n */\n public function importBusinessProcesses(): void\n {\n $query = '\n SELECT\n Id, IsActive, Name, TableEnumOrId\n FROM\n BusinessProcess\n WHERE\n TableEnumOrId IN (\\'Lead\\',\\'Opportunity\\')';\n\n try {\n $sfProcesses = $this->queryHandler->query($query);\n\n // Upsert all processes for the team.\n foreach ($sfProcesses as $sfProcess) {\n /** @var BusinessProcess $businessProcess */\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $sfProcess['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => $sfProcess['Name'],\n 'type' => $sfProcess['TableEnumOrId'] === 'Lead' ? 'lead' : 'opportunity',\n 'is_selectable' => $sfProcess['IsActive'],\n ]);\n\n $this->importBusinessProcessStages($businessProcess);\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * Gets business process stages.\n */\n private function importBusinessProcessStages(BusinessProcess $businessProcess): void\n {\n $query = '\n SELECT\n Metadata\n FROM\n BusinessProcess\n WHERE\n Id = :processId';\n\n try {\n $stages = [];\n $sfProcessStages = $this->queryHandler->metadata($query, [\n 'processId' => $businessProcess->crm_provider_id,\n ]);\n\n // There is always 1 result at this point.\n $sfProcessStage = $sfProcessStages->current();\n\n // Upsert all processes for the team.\n foreach ($sfProcessStage['Metadata']['values'] as $sfProcessStage) {\n $sanitizedName = urldecode($sfProcessStage['valueName']); // Must decode: \"%2C\" becomes \",\" etc.\n\n $stage = $businessProcess->crm->stages()\n // This MUST match on label because this API doesn't use API Name.\n ->where('label', $sanitizedName)\n ->where('type', $businessProcess->type)\n ->where('is_selectable', 1)\n ->first();\n\n if ($stage) {\n $stages[] = $stage->id;\n }\n }\n\n $businessProcess->stages()->sync($stages);\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * Gets record types.\n */\n public function importRecordTypes(): void\n {\n $query = '\n SELECT\n Id, IsActive, Name, BusinessProcessId, SobjectType\n FROM\n RecordType';\n\n try {\n $sfRecordTypes = $this->queryHandler->query($query);\n\n // Upsert all record types for the process.\n foreach ($sfRecordTypes as $sfRecordType) {\n $businessProcess = null;\n if ($sfRecordType['BusinessProcessId']) {\n $businessProcess = $this->config->businessProcesses()\n ->where('crm_provider_id', $sfRecordType['BusinessProcessId'])\n ->first();\n }\n\n /** @var RecordType $recordType */\n $recordType = $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $sfRecordType['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'type' => mb_strtolower($sfRecordType['SobjectType']),\n 'name' => $sfRecordType['Name'],\n 'is_selectable' => $sfRecordType['IsActive'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n $this->importRecordTypeFieldValues($recordType);\n }\n } catch (NoResultsException $noResultsException) {\n // Do nothing.\n }\n }\n\n /**\n * Import record type - field value mappings. This only works for standard fields.\n */\n private function importRecordTypeFieldValues(RecordType $recordType): void\n {\n try {\n $query = '\n SELECT\n Metadata\n FROM\n RecordType\n WHERE\n Id = :recordTypeId';\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'recordTypeId' => $recordType->crm_provider_id,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Sync field metadata.\n $picklists = $sfField['Metadata']['picklistValues'];\n\n foreach ($picklists as $picklist) {\n $field = $this->config->fields()->where([\n 'type' => Field::TYPE_PICKLIST,\n 'object_type' => $recordType->type,\n 'crm_provider_id' => $picklist['picklist'],\n ])->first();\n\n if ($field) {\n $fieldValues = [];\n\n foreach ($picklist['values'] as $value) {\n // Must decode: \"%2C\" becomes \",\" etc.\n $fieldValue = $field->values()\n ->where('value', urldecode($value['valueName']))\n ->first();\n\n if ($fieldValue) {\n $fieldValues[] = $fieldValue->id;\n }\n }\n\n $recordType->fieldValues()->sync($fieldValues);\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $params = [];\n $missingStage = null;\n if ($types === null) {\n $types = [Stage::TYPE_LEAD, Stage::TYPE_OPPORTUNITY];\n }\n\n foreach ($types as $type) {\n if ($type === Stage::TYPE_LEAD) {\n $query = '\n SELECT\n Id, ApiName, MasterLabel, SortOrder\n FROM\n LeadStatus';\n } else {\n $query = '\n SELECT\n Id, ApiName, MasterLabel, IsActive, SortOrder, DefaultProbability\n FROM\n OpportunityStage';\n }\n\n if ($missingStageName) {\n $escapedStageName = ValueNormalizer::replaceQueryWithStringLiterals($missingStageName);\n\n $query .= ' WHERE ApiName = :stageName';\n\n $params = [\n 'stageName' => $escapedStageName,\n ];\n }\n\n try {\n $sfStages = $this->queryHandler->query($query, $params);\n } catch (NoResultsException $exception) {\n $sfStages = [];\n }\n\n $missingStage = null;\n\n // Upsert all stages for the team.\n foreach ($sfStages as $sfStage) {\n $selectable = true;\n if (array_key_exists('IsActive', $sfStage)) {\n $selectable = $sfStage['IsActive'];\n }\n\n $this->restoreAnyTrashedEntity($this->config->stages(), $sfStage['Id']);\n\n $stage = $this->config->stages()->updateOrCreate([\n 'crm_provider_id' => $sfStage['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($sfStage['ApiName'], 0, 50),\n 'label' => mb_strimwidth($sfStage['MasterLabel'], 0, 191),\n 'type' => $type,\n 'sequence' => $sfStage['SortOrder'] ?? 0,\n 'is_selectable' => $selectable,\n 'probability' => $sfStage['DefaultProbability'] ?? null,\n ]);\n\n if ($missingStageName && $missingStageName === $sfStage['ApiName']) {\n $missingStage = $stage;\n }\n }\n\n if ($missingStageName && $missingStage === null) {\n // If they requested a stage that still doesn't exist, it must be inactive so lazy create it.\n $missingStage = $this->config->stages()->create([\n 'crm_provider_id' => Uuid::uuid4(),\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($missingStageName, 0, 50),\n 'label' => mb_strimwidth($missingStageName, 0, 191),\n 'type' => $type,\n 'sequence' => 0,\n 'is_selectable' => 0,\n ]);\n }\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('lead');\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Lead\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfLeads = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfLeads as $sfLead) {\n // Only sync if previously imported.\n if ($this->hasLead($sfLead['Id'])) {\n $this->importLead($sfLead);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::LEAD);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n $fields = $this->getAllFieldsAsArray('lead');\n\n $sfLead = $this->getRecord('Lead', $crmId, $fields);\n\n return $this->importLead($sfLead);\n }\n\n private function importLead($crmData): ?Lead\n {\n /** @var ?Stage $stage */\n $stage = null;\n if (isset($crmData['Status'])) {\n // Get the current stage.\n $stage = $this->config\n ->stages()\n ->where('name', $crmData['Status'])\n ->where('type', Stage::TYPE_LEAD)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_LEAD], $crmData['Status']);\n }\n }\n\n // If we have no way of importing this, just return null :(\n if ($stage === null) {\n return null;\n }\n\n $countryCode = $crmData['CountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country name.\n if ($countryCode === null && empty($crmData['Country']) !== false) {\n $countryCode = $this->convertCountryNameToCode($crmData['Country']);\n }\n\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['Phone'] ?? '', 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n\n $mobilePhone = null;\n if (empty($crmData['MobilePhone']) === false) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['MobilePhone'], 0, 25);\n $mobilePhone = phone_e164($countryCode, $number);\n }\n\n $convertedDate = null;\n $convertedAccount = null;\n $convertedOpportunity = null;\n $convertedContact = null;\n\n if ($crmData['IsConverted'] == 'true') {\n $convertedDate = $crmData['ConvertedDate'];\n\n if (empty($crmData['ConvertedAccountId']) === false) {\n $convertedAccount = $this->config\n ->accounts()\n ->where('crm_provider_id', $crmData['ConvertedAccountId'])\n ->first();\n\n if ($convertedAccount === null) {\n try {\n $convertedAccount = $this->syncAccount($crmData['ConvertedAccountId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n\n if (empty($crmData['ConvertedOpportunityId']) === false) {\n $convertedOpportunity = $this->config\n ->opportunities()\n ->where('crm_provider_id', $crmData['ConvertedOpportunityId'])\n ->first();\n\n if ($convertedOpportunity === null) {\n try {\n $convertedOpportunity = $this->syncOpportunity($crmData['ConvertedOpportunityId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n\n if (empty($crmData['ConvertedContactId']) === false) {\n $convertedContact = $this->team\n ->crm\n ->contacts()\n ->where('crm_provider_id', $crmData['ConvertedContactId'])\n ->first();\n\n if ($convertedContact === null) {\n try {\n $convertedContact = $this->syncContact($crmData['ConvertedContactId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n }\n\n if (empty($crmData['Company'])) {\n $company = 'Unknown';\n } else {\n $company = mb_strimwidth($crmData['Company'], 0, 191);\n }\n\n $domain = null;\n if (empty($crmData['Website']) === false) {\n $domain = mb_strimwidth($crmData['Website'], 0, 191);\n $domain = StringUtil::resolveDomain($domain);\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'] ?? '',\n 'company' => $company,\n 'domain' => $domain,\n 'name' => $crmData['Name'] ? mb_strimwidth($crmData['Name'], 0, 191) : '',\n 'title' => $crmData['Title'] ? mb_strimwidth($crmData['Title'], 0, 128) : null,\n 'email' => $crmData['Email'] ? mb_strimwidth($crmData['Email'], 0, 80) : null,\n 'phone' => $parsedNumber['phone'],\n 'ext' => $parsedNumber['ext'] ?? null,\n 'mobile_phone' => $mobilePhone,\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Lead::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'stage_id' => $stage->id,\n 'record_type_id' => null,\n 'converted_at' => $convertedDate,\n 'converted_account_id' => $convertedAccount->id ?? null,\n 'converted_opportunity_id' => $convertedOpportunity->id ?? null,\n 'converted_contact_id' => $convertedContact->id ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->leads(), $crmData['Id']);\n\n /** @var Lead $lead */\n $lead = $this->config->leads()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n if ($lead->wasChanged('converted_at') && $lead->getConvertedAt() !== null) {\n $this->eventDispatcher->dispatch(new LeadConverted($lead));\n }\n\n $this->handleObjectDeletion($lead, $crmData);\n\n if ($lead->trashed()) {\n return null;\n }\n\n return $lead;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('account');\n\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Account\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfAccounts = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfAccounts as $sfAccount) {\n // Only sync if previously imported.\n if ($this->hasAccount($sfAccount['Id'])) {\n $this->importAccount($sfAccount);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::ACCOUNT);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n $fields = $this->getAllFieldsAsArray('account');\n if (! in_array('Id', $fields, true)) {\n $this->logger->info('[Salesforce] Sync account cancelled. Fields are not available.', [\n 'crmId' => $crmId,\n 'userId' => $this->profile->getUserId(),\n ]);\n\n return null;\n }\n\n $sfAccount = $this->getRecord('Account', $crmId, $fields);\n\n return $this->importAccount($sfAccount);\n }\n\n private function importAccount($crmData): ?Account\n {\n if (! empty($crmData['IsDeleted'])) {\n $this->handleEntityDeletionByProviderId($this->config->accounts(), $crmData);\n\n return null;\n }\n\n $countryCode = $crmData['BillingCountryCode'] ?? $crmData['ShippingCountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country names.\n if ($countryCode === null && empty($crmData['BillingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['BillingCountry']);\n }\n\n if ($countryCode === null && empty($crmData['ShippingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['ShippingCountry']);\n }\n\n if (empty($crmData['Phone']) === false) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['Phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $industry = null;\n if (empty($crmData['Industry']) === false) {\n $industry = mb_strimwidth($crmData['Industry'], 0, 40);\n }\n\n $domain = null;\n if (empty($crmData['Website']) === false) {\n $domain = mb_strimwidth($crmData['Website'], 0, 191);\n $domain = StringUtil::resolveDomain($domain);\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'],\n 'name' => mb_strimwidth($crmData['Name'], 0, 191),\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Account::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'industry' => $industry,\n 'domain' => $domain,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->accounts(), $crmData['Id']);\n\n /** @var Account $account */\n $account = $this->config->accounts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n $this->handleObjectDeletion($account, $crmData);\n\n return $account->trashed() ? null : $account;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOpportunities(array $parameters, ?string $strategy = null): int\n {\n $strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);\n\n $syncCount = 0;\n $logParams = $parameters;\n $parameters['profile'] = $this->profile;\n $logParams['user'] = $this->profile->getUserId();\n\n if (count($strategies) > 1) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Multiple sync strategies used', [\n 'teamId' => $this->team->getUuid(),\n 'params' => $logParams,\n 'strategies_count' => count($strategies),\n ]);\n }\n\n foreach ($strategies as $syncStrategy) {\n $name = $syncStrategy->getStrategyName();\n\n try {\n $sfOpportunities = $syncStrategy->fetchOpportunities($parameters);\n $totalRecords = $sfOpportunities->count();\n\n foreach ($sfOpportunities as $sfOpportunity) {\n $this->importOpportunity($sfOpportunity);\n $syncCount++;\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n $this->logger->warning('[' . $this->getDisplayName() . '] No opportunities found', [\n 'teamId' => $this->team->getUuid(),\n 'name' => $name,\n 'params' => $logParams,\n 'reason' => $noResultsException->getMessage(),\n ]);\n } catch (CrmException $crmException) {\n // Nothing to sync.\n $this->logger->warning('[' . $this->getDisplayName() . '] Opportunity sync failed', [\n 'teamId' => $this->team->getUuid(),\n 'name' => $name,\n 'params' => $logParams,\n 'reason' => $crmException->getMessage(),\n ]);\n }\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::OPPORTUNITY, ['params' => $logParams]);\n\n // debug to see how if count of opportunities reaches 1000\n if ($syncCount >= 1000) {\n $this->logger->info(\n '[' . $this->getDisplayName() . '] Sync Opportunities - count warning',\n [\n 'team_id' => $this->team->getId(),\n 'params' => $logParams,\n 'count' => $syncCount,\n 'strategies_count' => count($strategies),\n 'total_records' => $totalRecords ?? null,\n ]\n );\n }\n\n return $syncCount;\n }\n\n\n /**\n * @inheritdoc\n */\n public function syncOpportunity(string $crmId): ?Opportunity\n {\n $strategy = $this->opportunitySyncStrategyResolver->resolve(\n $this->config,\n OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY\n );\n\n $parameters = [\n 'profile' => $this->profile,\n 'crm_id' => $crmId,\n ];\n\n try {\n $sfOpportunity = $strategy->fetchOpportunities($parameters);\n } catch (HttpNotFoundException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [\n 'teamId' => $this->team->id_string,\n 'crmId' => $crmId,\n ]);\n\n return null;\n } catch (CrmException $crmException) {\n $this->logger->info('[' . $this->getDisplayName() . '] Opportunity sync failed', [\n 'teamId' => $this->team->id_string,\n 'crmId' => $crmId,\n 'exception' => $crmException->getMessage(),\n ]);\n\n return null;\n }\n\n if ($sfOpportunity instanceof ArrayIterator) {\n return $this->importOpportunity($sfOpportunity->getItems());\n }\n\n return $this->importOpportunity($sfOpportunity);\n }\n\n /**\n * @throws HttpNotFoundException\n */\n private function importOpportunity($crmData): ?Opportunity\n {\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $account = null;\n if (empty($crmData['AccountId']) === false) {\n /** @var ?Account $account */\n $account = $this->config->accounts()\n ->where('crm_provider_id', (string) $crmData['AccountId'])\n ->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmData['AccountId']);\n }\n }\n\n $userId = $profile?->getUserId() ?? $account?->getUserId();\n if ($userId === null) {\n $this->logger->error('[Salesforce] | Skip import, no user_id found', [\n 'id' => $crmData['Id'],\n ]);\n\n return null;\n }\n\n /** @var ?Stage $stage */\n $stage = null;\n if (isset($crmData['StageName'])) {\n $stage = $this->config\n ->stages()\n ->where('name', $crmData['StageName'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->orderBy('is_selectable', 'DESC')\n ->orderBy('id')\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $crmData['StageName']);\n }\n }\n\n $recordType = null;\n if (empty($crmData['RecordTypeId']) === false) {\n /** @var ?RecordType $recordType */\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $crmData['RecordTypeId'])\n ->first();\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $closeDate = null;\n if (empty($crmData['CloseDate']) === false) {\n $closeDate = Carbon::parse($crmData['CloseDate'])->format('Y-m-d');\n }\n\n $valueFieldName = 'Amount';\n if ($this->config->opportunity_value_field_id) {\n $valueFieldName = $this->config->opportunityValueField->crm_provider_id;\n }\n\n $data = [\n 'team_id' => $this->team->id,\n 'account_id' => $account->id ?? null,\n 'user_id' => $userId,\n 'owner_id' => $crmData['OwnerId'] ?? null,\n 'name' => mb_strimwidth($crmData['Name'] ?? '', 0, 128),\n 'value' => $crmData[$valueFieldName],\n 'currency_code' => CurrencyFormatter::formatCode($crmData['CurrencyIsoCode'] ?? null),\n 'close_date' => $closeDate,\n 'is_closed' => $crmData['IsClosed'],\n 'is_won' => $crmData['IsWon'],\n 'stage_id' => $stage?->id ?? null,\n 'record_type_id' => $recordType->id ?? null,\n 'remotely_created_at' => $createdDate,\n 'probability' => $crmData['Probability'] ?? null,\n 'forecast_category' => $crmData['ForecastCategoryName'] ?? null,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->opportunities(), $crmData['Id']);\n\n // Do not allow locked DB tables & other errors\n // to interrupt the process of reverting the trashed opportunities\n try {\n /** @var Opportunity $opportunity */\n $opportunity = $this->config->opportunities()\n ->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n // import external fields into crm_field_data if present\n $crmFields = $this->getOpportunitySyncableFields();\n\n $this->importOpportunityCrmFieldData($crmData, $crmFields, $opportunity->id);\n\n $this->handleObjectDeletion($opportunity, $crmData);\n\n if ($opportunity->trashed()) {\n return null;\n }\n\n if ($opportunity->wasRecentlyCreated) {\n MatchActivitiesToNewOpportunity::dispatch($opportunity->getId());\n }\n\n return $opportunity;\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->error('[Salesforce] importOpportunity failure.', [\n 'crm_provider_id' => $crmData['Id'],\n 'team_id' => $this->team->id,\n 'exception' => $exception->getMessage(),\n ]);\n\n $this->handleEntityDeletionByProviderId($this->config->opportunities(), $crmData);\n }\n\n return null;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('contact');\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Contact\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfContacts = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfContacts as $sfContact) {\n // Only sync if previously imported.\n if ($this->hasContact($sfContact['Id'])) {\n $this->importContact($sfContact);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::CONTACT);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n $fields = $this->getAllFieldsAsArray('contact');\n if (! in_array('Id', $fields, true)) {\n $this->logger->info('[Salesforce] Sync contact cancelled. Fields are not available.', [\n 'crmId' => $crmId,\n 'userId' => $this->profile->getUserId(),\n ]);\n\n return null;\n }\n\n $sfContact = $this->getRecord('Contact', $crmId, $fields);\n\n return $this->importContact($sfContact);\n }\n\n private function importContact($crmData): ?Contact\n {\n if (! empty($crmData['IsDeleted'])) {\n $this->handleEntityDeletionByProviderId($this->config->contacts(), $crmData);\n\n return null;\n }\n\n $account = $this->resolveContactAccount($crmData);\n $countryCode = $this->resolveContactCountryCode($crmData, $account);\n [$parsedNumber, $ext] = $this->parseContactPhone($countryCode, $crmData);\n $mobileNumber = $this->parseContactMobile($countryCode, $crmData);\n $createdDate = empty($crmData['CreatedDate'])\n ? null\n : Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'account_id' => $account->id ?? null,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'] ?? null,\n 'name' => ($crmData['Name'] ?? null) !== null ? mb_strimwidth($crmData['Name'], 0, 100) : '',\n 'title' => ($crmData['Title'] ?? null) !== null ? mb_strimwidth($crmData['Title'], 0, 128) : null,\n 'email' => ($crmData['Email'] ?? null) !== null ? mb_strimwidth($crmData['Email'], 0, 191) : null,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $ext,\n 'mobile_phone' => $mobileNumber,\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Contact::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->contacts(), $crmData['Id']);\n\n /** @var Contact $contact */\n $contact = $this->config->contacts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n $this->handleObjectDeletion($contact, $crmData);\n\n return $contact->trashed() ? null : $contact;\n }\n\n private function resolveContactAccount(array $crmData): ?Account\n {\n if (! isset($crmData['AccountId'])) {\n return null;\n }\n\n return $this->config->accounts()\n ->where('crm_provider_id', (string) $crmData['AccountId'])\n ->first() ?? $this->syncAccount($crmData['AccountId']);\n }\n\n private function resolveContactCountryCode(array $crmData, ?Account $account): ?string\n {\n $countryCode = $crmData['MailingCountryCode'] ?? null;\n\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n if ($countryCode === null && empty($crmData['MailingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['MailingCountry'])\n ?? ($account?->country_code);\n }\n\n return $countryCode;\n }\n\n private function parseContactPhone(?string $countryCode, array $crmData): array\n {\n if (empty($crmData['Phone'])) {\n return [[], null];\n }\n\n $parsedNumber = parsePhoneNumber($countryCode, Str::limit($crmData['Phone'], 25, ''));\n $ext = empty($parsedNumber['ext']) ? null : Str::limit($parsedNumber['ext'], 10, '');\n\n return [$parsedNumber, $ext];\n }\n\n private function parseContactMobile(?string $countryCode, array $crmData): ?string\n {\n if (empty($crmData['MobilePhone'])) {\n return null;\n }\n\n return Str::limit(phone_e164($countryCode, $crmData['MobilePhone']), 25, '');\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n $fields = [\n 'InstanceName',\n 'OrganizationType',\n 'IsSandbox',\n ];\n\n $orgValues = $this->getRecord('Organization', $this->config->crm_provider_id, $fields);\n\n $edition = null;\n switch ($orgValues['OrganizationType']) {\n case 'Developer Edition':\n $edition = Configuration::EDITION_DEVELOPER;\n\n break;\n\n case 'Professional Edition':\n $edition = Configuration::EDITION_PROFESSIONAL;\n\n break;\n\n case 'Enterprise Edition':\n $edition = Configuration::EDITION_ENTERPRISE;\n\n break;\n }\n\n $this->config->edition = $edition;\n $this->config->instance = $orgValues['InstanceName'];\n\n // XXX: How can this state be possible?\n if ($this->config->version === null) {\n $this->config->version = Client::MIN_API_VERSION;\n }\n\n $installedVersion = $this->getInstalledAppVersion();\n if ($installedVersion !== null) {\n $installedVersion = (string) $this->getInstalledAppVersion();\n }\n\n $this->config->installed_app_version = $installedVersion;\n\n $this->config->save();\n }\n\n public function getInstalledAppVersion(): ?string\n {\n try {\n $query = '\n SELECT\n SubscriberPackageVersion.MajorVersion,\n SubscriberPackageVersion.MinorVersion,\n SubscriberPackageVersion.PatchVersion,\n SubscriberPackageVersion.BuildNumber\n FROM\n InstalledSubscriberPackage\n WHERE\n SubscriberPackageId = :packageId\n ';\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'packageId' => self::INSTALLED_PACKAGE_ID,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Grab version number.\n $version = $sfField['SubscriberPackageVersion']['MajorVersion'] .\n $sfField['SubscriberPackageVersion']['MinorVersion'] .\n $sfField['SubscriberPackageVersion']['PatchVersion'] .\n $sfField['SubscriberPackageVersion']['BuildNumber'];\n } catch (\\Exception) {\n $version = null;\n }\n\n return $version;\n }\n\n /**\n * Store transcripts as note.\n *\n * @throws \\Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For SF we also check if Log Notes is enabled.\n if ($this->profile->log_notes === Profile::LOG_NOTE_NONE) {\n return;\n }\n\n if ($activity->opportunity_id && $activity->prospect === null) {\n return;\n }\n\n try {\n $transcriptionData = $this->generateTranscription($activity);\n\n $noteMaxLength = $this->profile->log_notes === Profile::LOG_NOTE_ENHANCED\n ? self::ENHANCED_NOTE_MAX_LENGTH\n : self::CLASSIC_NOTE_MAX_LENGTH;\n\n $title = 'Transcript for ';\n $title .= $activity->title ?? $activity->activity_title;\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($transcriptionData, 0, $noteMaxLength);\n\n if ($activity->opportunity_id) {\n $objectId = $activity->opportunity->crm_provider_id;\n } else {\n $objectId = $activity->prospect->crm_provider_id;\n }\n\n $noteId = $this->saveNote($title, $body, $objectId);\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n }\n\n public function saveNote(string $title, string $body, string $objectId, ?NoteObject $noteObject = null): ?string\n {\n $noteId = null;\n\n try {\n if ($this->profile->log_notes === Profile::LOG_NOTE_ENHANCED) {\n $noteId = $this->buildEnhancedNote($title, $body, $objectId);\n } else {\n $noteId = $this->buildClassicNote($title, $body, $objectId);\n }\n } catch (HttpNotFoundException $exception) {\n // The profile not having access to create Enhanced Notes. Set their preference to Classic.\n if ($this->profile->log_notes === Profile::LOG_NOTE_ENHANCED) {\n $this->profile->update([\n 'log_notes' => Profile::LOG_NOTE_CLASSIC,\n ]);\n }\n }\n\n return $noteId;\n }\n\n /**\n * This is using the \"Enhanced\" Notes feature, NOT the \"Notes & Attachments\" feature being deprecated.\n *\n * @url https://salesforce.stackexchange.com/questions/104408/how-can-i-create-an-account-note-or-contact-note-via-api-that-is-visible-in-sale\n */\n private function buildEnhancedNote(string $title, string $body, string $objectId): string\n {\n // Decode stored entities, escape HTML (without quoting), then convert line breaks for Salesforce formatting\n $decodedBody = html_entity_decode($body, ENT_QUOTES | ENT_HTML5);\n $sanitizedBody = htmlspecialchars($decodedBody, ENT_NOQUOTES, 'UTF-8', false);\n $content = nl2br($sanitizedBody, false);\n $note = [\n 'OwnerId' => $this->profile->crm_provider_id,\n 'Title' => $title,\n 'Content' => base64_encode($content),\n ];\n\n $noteId = $this->createRecord('ContentNote', $note);\n\n $link = [\n 'ContentDocumentId' => $noteId,\n 'LinkedEntityId' => $objectId,\n 'ShareType' => 'I',\n ];\n\n $this->createRecord('ContentDocumentLink', $link);\n\n return $noteId;\n }\n\n private function buildClassicNote(string $title, string $body, string $objectId): string\n {\n if (in_array($this->parseObjectType($objectId), [Field::OBJECT_TASK, Field::OBJECT_EVENT])) {\n $this->logger->info('[Salesforce] Summary not sent', [\n 'profile_id' => $this->profile->id,\n 'objectId' => $objectId,\n 'reason' => 'Classical Note does not support Task/Event relation',\n ]);\n\n return '';\n }\n\n $titleTrimmed = null;\n\n if (mb_strlen($title) > 80) {\n $titleTrimmed = substr($title, 0, 77) . '...';\n }\n $payload = [\n 'OwnerId' => $this->profile->crm_provider_id,\n 'IsPrivate' => false,\n 'Title' => $titleTrimmed ?? $title,\n 'Body' => $titleTrimmed ? $title . PHP_EOL . $body : $body,\n 'ParentId' => $objectId,\n ];\n\n return $this->createRecord('Note', $payload);\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n if ($this->profile === null) {\n return [];\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $limitValues = ['limit' => $this->limit, 'offset' => $this->offset];\n $sosl = $queryBuilder->buildFindQuery($name, $scopes, $limitValues);\n\n $this->logger->info('[Salesforce] Find prospects', [\n 'profile_id' => $this->profile->id,\n 'sosl_query' => $sosl,\n 'search_string' => $name,\n 'scopes' => $scopes,\n ]);\n\n $data = Cache::remember($this->profile->id . $sosl, self::CACHE_TTL, function () use ($sosl) {\n $data = [];\n\n try {\n // Hit remote API.\n $objects = $this->queryHandler->search($sosl);\n\n // Build mapped list.\n foreach ($objects as $object) {\n $type = strtolower($object['attributes']['type']);\n\n $record = [\n 'crmId' => $object['Id'],\n 'name' => $object['Name'],\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n 'crmUrl' => $this->generateProviderUrl($object['Id'], $type),\n ];\n\n switch ($type) {\n case 'lead':\n if (empty($object['Company']) === false) {\n $record['organization'] = $object['Company'];\n }\n\n if (empty($object['Title']) === false) {\n $record['title'] = $object['Title'];\n }\n\n $stage = $this->config->stages()\n ->where('type', Stage::TYPE_LEAD)\n ->where('name', $object['Status'])\n ->first();\n\n // Lazy create the stage.\n if ($stage === null) {\n $stage = $this->importStages([Stage::TYPE_LEAD], $object['Status']);\n }\n\n if ($stage) {\n $record += [\n 'stage' => [\n 'id' => $stage->id_string,\n 'name' => $stage->name,\n ],\n ];\n }\n\n if (empty($object['RecordTypeId']) === false) {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $object['RecordTypeId'])\n ->first();\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n }\n\n break;\n\n case 'account':\n if (empty($object['Industry']) === false) {\n $record['industry'] = $object['Industry'];\n $record['detailsLine'] = $object['Industry'];\n }\n if (! empty($object['PersonEmail'])) {\n $record['detailsLine'] = $object['PersonEmail'];\n }\n\n break;\n\n case 'contact':\n // For contacts, we should try and fetch their account name too.\n if ($object['AccountId']) {\n // Cheaper to get this locally.\n $account = $this->config->accounts()\n ->where('crm_provider_id', $object['AccountId'])\n ->first(['name']);\n\n if ($account) {\n $record['organization'] = $account->name;\n }\n }\n\n if (! empty($object['IsPersonAccount']) && $object['Email']) {\n $record['detailsLine'] = $object['Email'];\n } else {\n if (empty($object['Title']) === false) {\n $record['title'] = $object['Title'];\n }\n }\n\n break;\n }\n\n // Add phone numbers to record.\n if (empty($object['Phone']) === false && $object['Phone']) {\n $record['phoneNumbers'][] = [\n 'number' => $object['Phone'],\n 'nationalFormat' => phone_national($this->profile->user->country_code, $object['Phone']),\n 'type' => 'phone',\n ];\n }\n\n if (empty($object['MobilePhone']) === false && $object['MobilePhone']) {\n $record['phoneNumbers'][] = [\n 'number' => $object['MobilePhone'],\n 'nationalFormat' => phone_national(\n $this->profile->user->country_code,\n $object['MobilePhone']\n ),\n 'type' => 'mobile',\n ];\n }\n\n $data[] = $record;\n }\n } catch (NoResultsException $e) {\n $data = [];\n }\n\n return $data;\n });\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n try {\n // Perhaps their profile has no opportunity permissions.\n if ($this->profile === null || $this->profile->opportunity_fields === null) {\n return $data;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildFindOpportunitiesQuery();\n\n $objects = $this->queryHandler->query($query, ['accountId' => $crmAccountId]);\n\n foreach ($objects as $object) {\n $record = [\n 'crmId' => $object['Id'],\n 'name' => $object['Name'],\n 'won' => $object['IsWon'],\n 'closed' => $object['IsClosed'],\n ];\n\n $valueFieldName = 'Amount';\n if ($this->config->opportunity_value_field_id) {\n $valueFieldName = $this->config->opportunityValueField->crm_provider_id;\n }\n\n if (empty($object[$valueFieldName]) === false) {\n $currency = $object['CurrencyIsoCode'] ?? $this->config->default_currency;\n $value = formatCurrency($object[$valueFieldName], $currency);\n\n $record += [\n 'value' => $value,\n ];\n }\n\n $stage = $this->config->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->where('name', $object['StageName'])\n ->first();\n\n // Lazy create the stage.\n if ($stage === null) {\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $object['StageName']);\n }\n\n $record += [\n 'stage' => [\n 'id' => $stage->id_string,\n 'name' => $stage->name,\n ],\n ];\n\n if (empty($object['RecordTypeId']) === false) {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $object['RecordTypeId'])\n ->first();\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n }\n\n if ($ownerId && isset($object['OwnerId']) && $object['OwnerId'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n } catch (NoResultsException $e) {\n return $data;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n public function getContactRolesFromCrm(?Carbon $since = null): array\n {\n $roles = [];\n\n if ($this->profile === null) {\n return $roles;\n }\n\n $queryBuilder = app(QueryBuilder::class, ['profile' => $this->profile]);\n\n $query = $queryBuilder->buildGetContactRolesQuery($since);\n\n try {\n $objects = $this->queryHandler->query($query);\n\n foreach ($objects as $object) {\n $roles[] = [\n 'id' => $object['Id'],\n 'contactId' => $object['ContactId'],\n 'opportunityId' => $object['OpportunityId'],\n 'ownerId' => $object['Opportunity']['OwnerId'] ?? null,\n 'isPrimary' => $object['IsPrimary'],\n 'role' => $object['Role'],\n ];\n }\n } catch (NoResultsException) {\n // Just return an empty array.\n $this->logger->info('[Salesforce] No contact roles found', [\n 'since' => $since?->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n }\n\n return $roles;\n }\n\n public function syncContactRoles(Carbon $since): int\n {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $crmContactRoles = $this->getContactRolesFromCrm(since: $since);\n $syncCount = 0;\n $contactRoles = [];\n\n foreach ($crmContactRoles as $crmContactRole) {\n $contactRoles[] = $this->importContactRole($crmContactRole);\n $syncCount++;\n }\n\n $contactRoleRepository->saveContactRoles($contactRoles);\n\n $this->syncRemotelyDeletedContactRoles();\n\n return $syncCount;\n }\n\n private function importContactRole(array $contactRole): array\n {\n $contact = $this->config->contacts()\n ->where('crm_provider_id', $contactRole['contactId'])\n ->first();\n\n if ($contact === null) {\n $contact = $this->syncContact($contactRole['contactId']);\n }\n\n $opportunity = $this->config->opportunities()\n ->where('crm_provider_id', $contactRole['opportunityId'])\n ->first();\n\n if ($opportunity === null) {\n $opportunity = $this->syncOpportunity($contactRole['opportunityId']);\n }\n\n $role = null;\n if (! empty($contactRole['role'])) {\n $role = mb_strimwidth($contactRole['role'], 0, 191);\n }\n\n return [\n 'crm_configuration_id' => $this->config->getId(),\n 'contact_id' => $contact->getId(),\n 'crm_provider_id' => $contactRole['id'],\n 'subject_type' => ContactRole::SUBJECT_TYPE_OPPORTUNITY,\n 'subject_id' => $opportunity->getId(),\n 'is_primary' => $contactRole['isPrimary'],\n 'role' => $role,\n ];\n }\n\n protected function syncRemotelyDeletedContactRoles(): bool\n {\n try {\n $deletedRemotely = $this->queryHandler->queryDeleted('OpportunityContactRole');\n } catch (NoResultsException $e) {\n return false;\n }\n\n $deletedOpportunities = $deletedRemotely->getResults();\n $deletedIds = array_column($deletedOpportunities, 'id');\n\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n foreach (array_chunk($deletedIds, self::HARD_DELETE_CHUNK) as $chunk) {\n $contactRoleRepository->deleteContactRoles($chunk);\n\n $this->logger->info('[' . $this->getDisplayName() . '] Remotely deleted opportunities synced', [\n 'teamId' => $this->team->id_string,\n 'remotelyDeletedOpportunities' => $chunk,\n 'count' => count($chunk),\n ]);\n }\n\n return true;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = $objects = [];\n\n $hasWho = \\in_array($objectType, ['lead', 'contact']);\n $playbook = $this->getPlaybook($this->profile->user);\n $fields = array_merge(\n $this->profile->getFieldsAsArray(parent::OBJECT_TASK),\n $playbook && $playbook->activityField ? [$playbook->activityField->crm_provider_id] : []\n );\n\n // Query should default to any open call for that user.\n $query = '\n SELECT ' . implode(',', array_unique($fields)) . '\n FROM Task\n WHERE OwnerId = :ownerId\n AND IsArchived = false\n AND IsDeleted = false\n AND IsClosed = false\n AND (';\n\n if ($objectType === 'account') {\n // This covers tasks tied to a related contact or opportunity too.\n $query .= '\n AccountId = :accountId';\n }\n\n if ($hasWho) {\n $query .= '\n WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($opportunityId) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ' ) ORDER BY LastModifiedDate DESC';\n\n try {\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $this->profile->crm_provider_id,\n 'whoId' => $objectId,\n 'whatId' => $opportunityId,\n 'accountId' => $objectId,\n ]);\n } catch (NoResultsException $e) {\n return $data;\n } finally {\n $this->logger->debug(sprintf('[Salesforce] Found %s tasks for query \"%s\"', count($objects), $query));\n }\n\n foreach ($objects as $object) {\n $dueDate = $object['ActivityDate'] ? Carbon::parse($object['ActivityDate'])->toIso8601String() : null;\n $data[] = [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'due' => $dueDate,\n 'type' => $object[$playbook->activityField->crm_provider_id],\n ];\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getEvents(string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = $objects = [];\n $user = $this->profile?->user;\n if ($this->profile === null || $user === null) {\n return $data;\n }\n\n $hasWho = \\in_array($objectType, ['lead', 'contact']);\n $playbook = $this->getPlaybook($user);\n $fields = array_merge(\n $this->profile->getFieldsAsArray(parent::OBJECT_EVENT),\n $playbook && $playbook->activityField ? [$playbook->activityField->crm_provider_id] : []\n );\n\n // Query should default to any event starting in the last week and ending up until today owned by the user.\n $query = '\n SELECT ' . implode(',', array_unique($fields)) . '\n FROM Event\n WHERE OwnerId = :ownerId\n AND IsArchived = false\n AND IsAllDayEvent = false\n AND StartDateTime >= LAST_N_DAYS:7\n AND EndDateTime <= TODAY\n AND (';\n\n if ($objectType === 'account') {\n // This covers events tied to a related contact or opportunity too.\n $query .= '\n AccountId = :accountId';\n }\n\n if ($hasWho) {\n $query .= '\n WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($opportunityId) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ' ) ORDER BY LastModifiedDate DESC';\n\n try {\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $this->profile->crm_provider_id,\n 'whoId' => $objectId,\n 'whatId' => $opportunityId,\n 'accountId' => $objectId,\n ]);\n } catch (NoResultsException $e) {\n return $data;\n } finally {\n $this->logger->debug(sprintf('[Salesforce] Found %s tasks for query \"%s\"', count($objects), $query));\n }\n\n foreach ($objects as $object) {\n $dueDate = $object['StartDateTime'] ? Carbon::parse($object['StartDateTime'])->toIso8601String() : null;\n\n $data[] = [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'due' => $dueDate,\n 'type' => $object[$playbook->activityField->crm_provider_id],\n ];\n }\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByQuery($email, Field::TYPE_EMAIL);\n if ($sosl === null) {\n return null;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $email,\n QueryHandler::PRIORITISE_EMAIL\n );\n\n $data = $this->convertCrmData($objects, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (NoResultsException $e) {\n // Try the account next.\n if ($this->profile->account_fields === null) {\n return null;\n }\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n // SF improved search - strip the domain extension, min domain name length 4\n return $this->getCompanyNameFromEmail(email: $email, minNameLength: 4);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByDomainQuery($companyName);\n\n try {\n $objects = $this->queryHandler->search($sosl);\n\n $data = $this->convertCrmData($objects, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (NoResultsException) {\n return null;\n }\n }\n\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n // Don't bother looking up numbers that are masked.\n if (str_contains($phone, '**')) {\n return null;\n }\n\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $phoneNational = phone_national(null, $phone) ?? '';\n $possiblePhoneFormats = collect([\n preg_replace('/\\D/', '', ltrim($phone, '0+')),\n preg_replace('/\\D/', '', $phoneNational),\n formatDashPhoneNumber($phone),\n $phoneNational,\n ])\n ->filter() // Removes null and empty strings\n ->unique()\n ->values();\n\n foreach ($possiblePhoneFormats as $phone) {\n $sosl = $queryBuilder->buildMatchByQuery($phone, Field::TYPE_PHONE);\n if ($sosl === null) {\n continue;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $phone,\n QueryHandler::PRIORITISE_PHONE\n );\n\n return $this->convertCrmData($objects, $userId);\n } catch (NoResultsException) {\n continue;\n }\n }\n\n return null;\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->profile->id . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /** Determine the CRM Objects which represent the call activity. */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n if ($this->profile === null) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByQuery($name, 'name');\n if ($sosl === null) {\n return false;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n } catch (NoResultsException $e) {\n return false;\n }\n\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $name,\n QueryHandler::PRIORITISE_NAME\n );\n\n $data = $this->convertCrmData($objects, $userId);\n\n return (! empty(array_filter($data))) ? $data : false;\n });\n\n return is_array($result) ? $result : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(QueryIterator $objects, ?int $userId = null): array\n {\n $lead = null;\n $contact = null;\n $opportunity = null;\n $account = null;\n $stage = null;\n $countryCode = null;\n\n if ($objects->count() > 0) {\n $object = $objects->current();\n\n if ($object['attributes']['type'] === 'Lead') {\n $lead = $this->importLead($object);\n\n // Lead might not be imported if the Stage is null for example.\n if ($lead) {\n $countryCode = $lead->country_code;\n $stage = $lead->stage;\n }\n } else {\n if ($object['attributes']['type'] === 'Contact') {\n $contact = $this->importContact($object);\n $account = $contact?->account;\n } else {\n $account = $this->importAccount($object);\n }\n\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account) {\n $countryCode = $account->country_code;\n }\n\n try {\n $sfOpportunities = $this->findOpportunities(\n $account?->getCrmProviderId(),\n $contact?->getCrmProviderId(),\n $userId\n );\n\n // Take the first opportunity, which will be ordered as priority based on their settings.\n if (! empty($sfOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($sfOpportunities[0]['crmId']);\n $stage = $opportunity?->stage;\n }\n } catch (Exception) {\n // Nothing to see here.\n }\n }\n }\n\n return [\n $lead,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n /**\n * @inheritdoc\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n if ($stage->type === Stage::TYPE_LEAD) {\n $objectType = 'Lead';\n $objectId = $crmObject->crm_provider_id;\n $objectStageType = 'Status';\n } else {\n $objectType = 'Opportunity';\n $objectId = $crmObject->crm_provider_id;\n $objectStageType = 'StageName';\n }\n\n $headers = [];\n if ($this->config->trigger_assignment_rules === false) {\n // @see: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/headers_autoassign.htm\n $headers = [\n 'Sforce-Auto-Assign' => 'false',\n ];\n }\n\n $this->updateRecord($objectType, $objectId, [$objectStageType => $stage->name], $headers);\n }\n\n public function parseObjectType(string $objectId): string\n {\n if (Str::startsWith($objectId, '001')) {\n return 'account';\n }\n\n if (Str::startsWith($objectId, '003')) {\n return 'contact';\n }\n\n if (Str::startsWith($objectId, '00Q')) {\n return 'lead';\n }\n\n if (Str::startsWith($objectId, '006')) {\n return 'opportunity';\n }\n\n if (Str::startsWith($objectId, '00U')) {\n return 'event';\n }\n\n if (Str::startsWith($objectId, '00T')) {\n return 'task';\n }\n\n throw new \\InvalidArgumentException('Unsupported Object Type');\n }\n\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, ['profile' => $this->profile]);\n $query = $queryBuilder->buildGetUsersQuery($userToSearch);\n\n try {\n $salesforceUsers = $this->queryHandler->query($query, [\n 'active' => true,\n ]);\n } catch (NoResultsException $e) {\n $this->logger->info('[Salesforce] Sync Profiles. No users found', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $teamRepository = app(TeamRepository::class);\n $customRules = $this->getCustomProfileRules($teamRepository);\n\n foreach ($salesforceUsers as $crmUser) {\n if ($crmUser['Email'] === null) {\n continue;\n }\n\n if (! $this->customProfileValidation($crmUser, $customRules)) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $crmUser['Email']);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $edition = $crmUser['UserPreferencesLightningExperiencePreferred']\n ? Profile::EDITION_LIGHTNING\n : Profile::EDITION_CLASSIC;\n\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->updateOrCreateProfile(\n $user,\n [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $crmUser['Id'],\n ],\n [\n 'user_id' => $user->getId(),\n 'edition' => $edition,\n 'has_external_cti' => ! empty($crmUser['CallCenterId']),\n 'crm_profile_id' => $crmUser['ProfileId'],\n ]\n );\n\n if ($userToSearch instanceof User && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n // Clean up inactive profiles\n try {\n $this->archiveInactiveProfiles();\n } catch (\\Exception $e) {\n $this->logger->warning('[Salesforce] Profile archiving failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n\n // For Salesforce it's easy, we just point every object to the apex domain and they handle it.\n switch ($objectType) {\n case 'lead':\n case 'account':\n case 'contact':\n case 'opportunity':\n case 'task':\n case 'event':\n case 'activity':\n\n $url = $this->config->crm_base_url . '/' . $providerId;\n\n break;\n }\n\n return $url;\n }\n\n public function buildTaskSearchFields(): array\n {\n return ['Id', 'WhoId', 'WhatId', 'AccountId'];\n }\n\n public function getTaskByFilterConditions(\n array $fields,\n array $filters,\n bool $bulkSearch = false,\n bool $strictFilters = true\n ): ?array {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildSearchTaskQuery($fields, $filters, $bulkSearch, $strictFilters);\n\n try {\n if (! $bulkSearch) {\n $objects = $this->queryHandler->query($query, $filters);\n if ($objects->count() === 1) {\n return $objects->current();\n }\n }\n\n if ($bulkSearch) {\n $objects = $this->queryHandler->query($query);\n $records = [];\n foreach ($objects as $record) {\n $key = $record[end($fields)];\n $records[$key] = $record;\n }\n\n return $records;\n }\n } catch (\\Exception $e) {\n $this->logger->info('[Salesforce] Failed to execute query', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function mapCrmObjects(array $task): array\n {\n $activityData = [];\n\n if (! empty($task['WhoId'])) {\n $type = $this->parseObjectType($task['WhoId']);\n $activityData[$type] = $task['WhoId'];\n }\n if (! empty($task['AccountId'])) {\n $activityData['account'] = $task['AccountId'];\n }\n if (! empty($task['WhatId'])) {\n $activityData['opportunity'] = $task['WhatId'];\n }\n\n return $activityData;\n }\n\n /**\n * Get SF task by Outreach call id.\n */\n public function getTaskByFilter(\n string $activityFieldType,\n array $filters,\n string $operator = '=',\n array $additionalFields = []\n ): ?array {\n $data = [];\n\n try {\n // Default (base) fields.\n $fields = ['Id', 'Subject', 'Description', 'ActivityDate', 'WhoId', 'WhatId', $activityFieldType];\n\n foreach ($additionalFields as $additionalField) {\n $fields[] = $additionalField->crm_provider_id;\n }\n\n $fields = array_unique($fields);\n\n // Find task with the same Outreach id as the call id.\n $query = 'SELECT ' . implode(',', $fields) . '\n FROM Task\n WHERE IsArchived = false AND IsDeleted = false';\n\n foreach ($filters as $key => $value) {\n $key = preg_quote($key, '/');\n $key = str_replace(['\\'', '\"'], '', $key);\n // Prepare the substitution.\n $strKey = \":$key\";\n\n $query .= \" AND $key $operator $strKey\";\n }\n\n $query .= ' ORDER BY LastModifiedDate DESC LIMIT 1';\n\n $objects = $this->queryHandler->query($query, $filters);\n\n // There should be only one task related to this call if any.\n if ($objects->count() === 1) {\n $object = $objects->current();\n\n $dueDate = $object['ActivityDate'] ? Carbon::parse($object['ActivityDate'])->toIso8601String() : null;\n\n $data = array_merge($object, [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'summary' => $object['Description'],\n 'due' => $dueDate,\n 'Type' => $object[$activityFieldType],\n ]);\n }\n } catch (NoResultsException $e) {\n // Filters don't match any records.\n } catch (ServiceUnavailableException $serviceUnavailableException) {\n // Service cannot be queried. We should probably log this.\n }\n\n return $data;\n }\n\n /**\n * Get Salesforce fields including datetime fields\n *\n * @param $objectType\n */\n private function getAllFieldsAsArray($objectType): array\n {\n $basicFields = [];\n // Not all users have access to all object fields.\n if ($this->profile->{$objectType . '_fields'}) {\n $basicFields = explode(',', $this->profile->{$objectType . '_fields'});\n }\n\n $extraFields = [\n 'CreatedDate',\n 'LastModifiedDate',\n 'IsDeleted',\n ];\n\n if ($objectType === self::OBJECT_OPPORTUNITY\n && $this->config->opportunity_value_field_id\n && ! in_array($this->config->opportunityValueField->crm_provider_id, $basicFields)\n ) {\n $extraFields[] = $this->config->opportunityValueField->crm_provider_id;\n }\n\n return array_unique(array_merge($basicFields, $extraFields));\n }\n\n /**\n * Generate transcription for activity description.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! ($this->config->store_transcript)) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n return $this->transcriptionService\n ->findTranscriptionByActivity($activity)\n ->map(static function (array $transcriptionSegment): string {\n return $transcriptionSegment['formattedStartsAt'] . ' | ' . $transcriptionSegment['transcript'];\n })\n ->implode(PHP_EOL);\n }\n\n /**\n * Find related Salesforce event based on activity data\n *\n * @return array<string>\n */\n public function fetchRelatedActivity(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Searching for related activity', [\n 'activityId' => $activity->getUuid(),\n 'ownerId' => $this->profile?->crm_provider_id,\n ]);\n\n $sfEvent = $this->fetchRelatedEvent($activity);\n if (empty($sfEvent)) {\n $this->logger->info('[Salesforce] No related activity found', [\n 'activityId' => $activity->getUuid(),\n 'ownerId' => $this->profile?->crm_provider_id,\n 'account' => $activity->hasAccount()\n ? $activity->getAccount()->getCrmProviderId()\n : null,\n ]);\n\n return [];\n }\n\n return $sfEvent;\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n if ($activity->isTypeConference() === false) {\n return null;\n }\n\n if ($activity->hasActualStartTime() === false && $activity->hasScheduledStartTime() === false) {\n return null;\n }\n\n if (! $activity->hasProspect()) {\n $this->logger->info('[Salesforce] Skip look up, Activity not linked to Lead, Contact or Account', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n $playbook = $this->getPlaybook($activity->getUser());\n if ($playbook !== null && $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK) {\n $this->logger->info('[Salesforce] Skip auto-sync for task-based playbook', [\n 'activityUuid' => $activity->getUuid(),\n 'playbookId' => $playbook->getId(),\n 'playbookType' => $playbook->getActivityType(),\n ]);\n\n return null;\n }\n\n try {\n $sfEvent = $this->fetchRelatedActivity($activity);\n if (empty($sfEvent)) {\n return null;\n }\n\n [$activityField, $activityType] = $this->resolveActivityTypeFromEvent($activity, $sfEvent);\n\n $this->logger->info('[Salesforce] Found related activity', [\n 'activityId' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n 'activityFieldName' => $activityField,\n 'crmActivityType' => ($activityField !== null && isset($sfEvent[$activityField]))\n ? $sfEvent[$activityField]\n : null,\n 'activityType' => $activityType,\n ]);\n\n $userId = $this->findRelatedActivityUserId($activity, $sfEvent);\n\n if ($activity->getUserId() !== $userId) {\n $this->logger->info('[Salesforce] Updating meeting owner', [\n 'activityId' => $activity->getUuid(),\n 'oldUserId' => $activity->getUserId(),\n 'newUserId' => $userId,\n ]);\n }\n\n $this->updateSfEventDescription($activity, $sfEvent);\n\n $activity->update([\n 'user_id' => $userId,\n 'crm_provider_id' => $sfEvent['Id'],\n 'playbook_category_id' => $activityType->id ?? $activity->getCategory()?->getId(),\n ]);\n\n $this->logger->info('[Salesforce] Activity updated', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return $activity;\n } catch (\\Exception $exception) {\n \\Sentry::captureException($exception);\n\n throw $exception;\n }\n }\n\n /**\n * @param array<string, mixed> $sfEvent\n *\n * @return array{0: string|null, 1: mixed}\n */\n private function resolveActivityTypeFromEvent(Activity $activity, array $sfEvent): array\n {\n $activityField = $this->getActivityFieldName($activity);\n $activityType = null;\n\n if ($activityField !== null && ! empty($sfEvent[$activityField])) {\n $playbook = $this->getPlaybook($activity->getUser());\n $activityType = $this->getPlaybookCategory($playbook, strval($sfEvent[$activityField]));\n }\n\n return [$activityField, $activityType];\n }\n\n /**\n * @param array<string> $sfEvent\n */\n private function findRelatedActivityUserId(Activity $activity, array $sfEvent): int\n {\n $userId = $activity->getUserId();\n\n if (empty($sfEvent['OwnerId']) === false) {\n $profile = $this\n ->config\n ->profiles()\n ->where('crm_provider_id', $sfEvent['OwnerId'])\n ->get()\n ->filter(static function (Profile $profile) use ($activity): bool {\n if (! $activity->isTypeConference()) {\n return ! empty($profile->user) ? $profile->user->isStatusActive() : false;\n }\n\n $participants = $activity->getParticipants();\n\n return ! empty($profile->user)\n ? $profile->user->isStatusActive()\n && $profile->user->hasPermission(PermissionEnum::RECORD_MEETING)\n && $participants->contains('user_id', $profile->user_id)\n : false;\n })\n ->first();\n\n if ($profile) {\n $userId = $profile->user_id;\n }\n }\n\n return $userId;\n }\n\n /**\n * @param array<string, mixed> $sfEvent\n */\n private function updateSfEventDescription(Activity $activity, array $sfEvent): void\n {\n try {\n if (str_contains($sfEvent['Description'], $activity->id_string)) {\n return;\n }\n\n $payload = [\n 'Description' => $sfEvent['Description']\n . PHP_EOL\n . PHP_EOL\n . new DecorateActivity()->generateDescription($activity),\n ];\n\n $this->logger->info('[Salesforce] Update record', [\n 'activityId' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n 'payload' => $payload,\n ]);\n\n $payload = array_merge(\n $payload,\n $this->payloadBuilder->fetchCustomFieldData($activity, Field::OBJECT_EVENT)\n );\n\n $this->updateRecord('Event', $sfEvent['Id'], $payload);\n } catch (\\Exception) {\n $this->logger->error('[Salesforce] Failed to update record', [\n 'activityUuid' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n ]);\n }\n }\n\n /**\n * Returns the most recently modified Event within time range (if any).\n *\n * @return array|null An Event record from Salesforce.\n */\n private function fetchRelatedEvent(Activity $activity): ?array\n {\n $ownerId = $this->profile?->crm_provider_id;\n if ($ownerId === null) {\n return [];\n }\n\n /** @var ?Carbon $from */\n /** @var ?Carbon $to */\n [$from, $to] = $this->getFromToDates($activity);\n\n try {\n $whoId = null;\n $hasWho = $activity->lead_id || $activity->contact_id;\n if ($hasWho) {\n $whoId = $activity->hasLead()\n ? $activity->getLead()->crm_provider_id\n : $activity->getContact()->crm_provider_id;\n }\n\n if ($hasWho === false && $activity->account_id === null) {\n return null;\n }\n\n $query = $this->buildFetchRelatedEventQuery($activity);\n\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $ownerId,\n 'whoId' => $whoId,\n 'whatId' => $activity->hasOpportunity() ? $activity->getOpportunity()->crm_provider_id : null,\n 'accountId' => $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null,\n 'from' => $from?->format('Y-m-d\\TH:i:s\\Z'),\n 'to' => $to?->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($objects as $object) {\n return $object;\n }\n } catch (NoResultsException $e) {\n return [];\n }\n\n return [];\n }\n\n private function getFromToDates(Activity $activity): array\n {\n $from = null;\n $to = null;\n\n /** @var ?CalendarEvent $calendarEvent */\n $calendarEvent = $activity->calendarEvent()->first();\n if ($calendarEvent !== null) {\n $from = $calendarEvent->getStartTime();\n $to = $calendarEvent->getEndTime();\n }\n\n // For non-calendar imported activities\n // Also double check if calendar event dates could be null?\n // If null use what we've got so far\n if ($from === null || $to === null) {\n $from = $activity->hasScheduledStartTime()\n ? $activity->getScheduledStartTime()\n : $activity->getActualStartTime();\n $to = $activity->hasScheduledEndTime()\n ? $activity->getScheduledEndTime()->addMinutes(15)\n : $activity->getActualEndTime();\n }\n\n return [$from, $to];\n }\n\n /**\n * Determines the appropriate activity field name for querying Salesforce events.\n *\n * This method follows a hierarchy to determine the field name:\n * 1. Uses the playbook's activity field if it exists and is in the profile's accessible fields\n * 2. Falls back to the default activity field if the profile has no event fields configured\n * 3. Returns null if no suitable field is found\n *\n * @param Activity $activity The activity to determine the field for\n *\n * @return string|null The field name to use in queries, or null if none is available\n */\n private function getActivityFieldName(Activity $activity): ?string\n {\n if ($this->profile === null) {\n $this->logger->warning('[Salesforce] Cannot determine activity field - profile not found', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n $profileEventFields = $this->profile->getFieldsAsArray('event');\n\n if (empty($profileEventFields)) {\n $defaultActivityField = $this->getDefaultActivityField(Field::OBJECT_EVENT);\n $defaultFieldName = $defaultActivityField?->getAttribute('crm_provider_id');\n // Profile not yet synced — fall back to the default activity field.\n // There is a small chance that the profile won't have Default Activity Type field access\n // in which case the query will fail.\n // This is however an edge case and should be reviewed for profile sync issues.\n Sentry::withScope(function (\\Sentry\\State\\Scope $scope) use ($defaultFieldName): void {\n $scope->setContext('details', [\n 'profileId' => $this->profile->id,\n 'defaultField' => $defaultFieldName,\n ]);\n Sentry::captureMessage(\n '[Salesforce] Profile event fields empty, falling back to default activity field.',\n \\Sentry\\Severity::warning()\n );\n });\n\n return $defaultFieldName;\n }\n\n $playbook = $this->getPlaybook($activity->getUser());\n\n if (! is_null($playbook) && ! is_null($playbook->getActivityField())) {\n $playbookFieldName = $playbook->getActivityField()->getAttribute('crm_provider_id');\n\n if (in_array($playbookFieldName, $profileEventFields, true)) {\n return $playbookFieldName;\n }\n\n $this->logger->warning('[Salesforce] Playbook activity field not found in profile fields', [\n 'activityId' => $activity->getUuid(),\n 'playbookField' => $playbookFieldName,\n 'profileId' => $this->profile->id,\n ]);\n }\n\n return null;\n }\n\n private function buildFetchRelatedEventQuery(Activity $activity): string\n {\n $hasWho = $activity->lead_id || $activity->contact_id;\n\n $activityFieldName = $this->getActivityFieldName($activity);\n $fields = array_filter(['Id', 'Description', 'OwnerId', $activityFieldName]);\n\n $ownerCondition = '(OwnerId = :ownerId OR CreatedById = :ownerId)';\n\n $query = '\n SELECT ' . implode(',', $fields) . '\n FROM Event\n WHERE ' . $ownerCondition . '\n AND IsArchived = false\n AND IsAllDayEvent = false\n AND StartDateTime >= :from\n AND EndDateTime <= :to\n AND (';\n\n $operator = '';\n if ($activity->account_id) {\n // This covers events tied to a related contact or opportunity too.\n $query .= 'AccountId = :accountId';\n\n $operator = ' OR ';\n }\n\n if ($hasWho) {\n $query .= $operator . 'WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($activity->opportunity_id) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ') ORDER BY LastModifiedDate DESC';\n\n return $query;\n }\n\n public function fetchProspect(array $task): array\n {\n $lead = $account = $opportunity = $contact = $stage = $countryCode = null;\n $externalId = $task['WhoId'] ?? null;\n\n // Lead or Contact\n if ($externalId) {\n try {\n [$lead, $account, $opportunity, $contact, $stage, $countryCode] = $this->parseRecords($externalId);\n } catch (\\InvalidArgumentException $exception) {\n // Invalid object type.\n }\n }\n\n // If we happen to know the opportunity or account from the Task, figure that out.\n if (empty($task['WhatId']) === false) {\n // WhatId could be either Account ID or Opportunity ID.\n // If WhatId is Opportunity ID, get the opportunity and stage from the CRM.\n try {\n [, $account, $opportunity, , $stage, ] = $this->parseRecords($task['WhatId']);\n } catch (\\InvalidArgumentException $exception) {\n // Invalid object type.\n }\n }\n\n return [$lead, $account, $opportunity, $contact, $stage, $countryCode];\n }\n\n /**\n * Save activity transcription summary as note\n */\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n return $this->saveNote($title, $body, (string) $objectId);\n }\n\n public function getObjectByFilterConditions(string $objectType, array $fields, array $filters): ?array\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildObjectSearchQuery($objectType, $fields, $filters);\n\n try {\n $objects = $this->queryHandler->query($query, $filters);\n if ($objects->count() === 1) {\n return $objects->current();\n }\n } catch (\\Exception $e) {\n $this->logger->info('[Salesforce] Failed to execute query', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n private function getCustomProfileRules(TeamRepository $teamRepository): array\n {\n $teamSettings = $teamRepository->getTeamSetting($this->team, 'custom_profile_validation');\n\n if ($teamSettings instanceof TeamSettings && $teamSettings->getValueType() === 'array') {\n $customRules = json_decode($teamSettings->getValue(), true);\n if (is_array($customRules)) {\n return $customRules;\n }\n }\n\n return [];\n }\n\n private function customProfileValidation(array $crmUser, array $customRules): bool\n {\n foreach ($customRules as $customRule) {\n if ($crmUser[$customRule['field']] !== $customRule['value']) {\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * When syncing Contact / Lead / Account / Opportunity / Stage crm entities,\n * validate and restore locally trashed objects,\n * before updating them. Objects are identified by CrmProviderId\n */\n private function restoreAnyTrashedEntity(HasMany $targetEntity, string $crmProviderId): void\n {\n $recordExists = $targetEntity->withTrashed()->where(['crm_provider_id' => $crmProviderId])->first();\n if ($recordExists && $recordExists->trashed()) {\n $recordExists->restore();\n }\n }\n\n #[\\Override] public function supportsNotes(): bool\n {\n return true;\n }\n\n private function getOwnerProfile(?string $ownerId): ?Profile\n {\n if ($ownerId === null) {\n return null;\n }\n\n return $this->config->profiles()\n ->where('crm_provider_id', $ownerId)\n ->first();\n }\n}","depth":4,"bounds":{"left":0.13863032,"top":0.12210695,"width":0.38597074,"height":0.87789303},"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Salesforce;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Events\\Dispatcher;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Component\\Country\\CountriesMap;\nuse Jiminny\\Contracts\\Acl\\PermissionEnum;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ImportsBusinessProcessesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\SalesforceBatchSyncInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\SalesforceInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteNoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SearchTaskInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SupportsObjectTypeParseInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmProfileRecordTypesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Enums\\CrmObject;\nuse Jiminny\\Events\\Activities\\Crm\\LeadConverted;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpBadRequestException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Exceptions\\NoResultsException;\nuse Jiminny\\Exceptions\\ServiceUnavailableException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Jobs\\Crm\\MatchActivitiesToNewOpportunity;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\ContactRole;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Crm\\RecordType;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\TeamSettings;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\ContactRoleRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\Crm\\RecordTypeFieldValuesRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Helpers\\ArrayIterator;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\FieldHelper;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\FieldTypeConverter;\nuse Jiminny\\Services\\Crm\\Salesforce\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\FollowupActivityTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\LogActivityTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\RecordManipulationsTrait;\nuse Jiminny\\Services\\Crm\\Salesforce\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse Jiminny\\Utils\\StringUtil;\nuse Ramsey\\Uuid\\Uuid;\nuse Sentry\\Laravel\\Facade as Sentry;\n\nclass Service extends BaseService implements\n SalesforceInterface,\n SalesforceBatchSyncInterface,\n SyncCrmEntitiesInterface,\n SyncCrmProfileRecordTypesInterface,\n ImportsBusinessProcessesInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SearchTaskInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n SupportsObjectTypeParseInterface,\n RemoteNoteEntityManipulationInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncFieldsTrait;\n use DeleteObjectsTrait;\n use RecordManipulationsTrait;\n use ServiceTraits\\BatchSyncTrait;\n use FollowupActivityTrait;\n use LogActivityTrait;\n\n /**\n * Note Body Limit for the Old Note-Taking Tool\n *\n * @var int\n */\n private const int CLASSIC_NOTE_MAX_LENGTH = 32000;\n\n /**\n * Note Content Limit for the New Notes\n *\n * @var int\n */\n private const int ENHANCED_NOTE_MAX_LENGTH = 50000000;\n\n private const string INSTALLED_PACKAGE_ID = '033Tw0000007bKbIAI';\n\n private const int CACHE_TTL = 600;\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day - 86400\n\n /**\n * @var Client\n */\n protected $client;\n\n protected PayloadBuilder $payloadBuilder;\n protected QueryHandler $queryHandler;\n\n private OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n\n public function __construct(\n Client $client,\n PayloadBuilder $payloadBuilder,\n protected Dispatcher $eventDispatcher,\n private readonly CountriesMap $countriesMap,\n private readonly ProspectPhotoPathService $prospectPhotoPathService,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->payloadBuilder = $payloadBuilder;\n $this->queryHandler = app(QueryHandler::class, [\n 'client' => $this->client,\n 'logger' => $this->logger,\n ]);\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n }\n\n public function getDisplayName(): string\n {\n return 'Salesforce';\n }\n\n public function getJobDelay(): int\n {\n return 1;\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n return $user->getSocialAccount(SocialAccount::PROVIDER_SALESFORCE);\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($activity, $crmProviderId) {\n $playbook = $this->getPlaybookFromActivity($activity);\n\n if ($playbook === null) {\n $this->logger->warning('[Salesforce] Cannot verify task - no playbook found', [\n 'activity' => $activity->getId(),\n 'crm_provider_id' => $crmProviderId,\n ]);\n\n return false;\n }\n\n $objectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_EVENT ? 'Event' : 'Task';\n\n try {\n $record = $this->getRecord($objectType, $crmProviderId, ['Id', 'IsDeleted']);\n\n return ! empty($record) && ($record['IsDeleted'] ?? false) === false;\n } catch (HttpNotFoundException|HttpBadRequestException) {\n $this->logger->info('[Salesforce] Activity record not found during verification', [\n 'activity' => $activity->getId(),\n 'object_type' => $objectType,\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n });\n }\n\n public function query(string $queryToRun, array $parameters = []): QueryIterator\n {\n // Due to poorly designed external calls, this method cannot be entirely removed\n return $this->queryHandler->query($queryToRun, $parameters);\n }\n\n /*=========== Organization Information ===============*/\n\n /**\n * Get a list of all the API Versions for the instance.\n *\n * @throws CrmException\n *\n * @return mixed\n *\n */\n public function getApiVersions()\n {\n $url = $this->config->crm_base_url . '/services/data';\n\n $response = $this->client->get($url);\n\n return json_decode($response->getBody(), true);\n }\n\n /**\n * Gets the valid recordTypes for a given Salesforce Object via the describe API.\n */\n private function getRecordTypes(string $crmObject): array\n {\n $url = $this->client->getObjectsUrl() . $crmObject . '/describe';\n\n $response = $this->client->get($url);\n $jsonResponse = json_decode($response->getBody(), true);\n\n $fields = [];\n foreach ($jsonResponse['recordTypeInfos'] as $row) {\n $fields[] = ['recordTypeId' => $row['recordTypeId'], 'default' => $row['defaultRecordTypeMapping']];\n }\n\n return $fields;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize($fieldType, $fieldValue, $internal);\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n $defaultFields = ($activityType === Playbook::ACTIVITY_TYPE_TASK)\n ? FieldDefinitions::defaultTaskFields()\n : FieldDefinitions::defaultEventFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n // Setup the activity field as the default Type.\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'Type',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK, Playbook::ACTIVITY_TYPE_EVENT];\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n\n $fieldFilter = ($activityType === Playbook::ACTIVITY_TYPE_TASK)\n ? FieldDefinitions::taskFollowupFieldsFilter()\n : FieldDefinitions::eventFollowupFieldsFilter();\n\n foreach ($fieldFilter as $eachFilter) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $eachFilter);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n /**\n * This one is now called only when ImportActivityTypes is triggered or SyncFieldMetadata executed manually\n * Regular sync now uses SharedSyncFieldsTrait -> syncSingleObjectType\n * Needs to be replaced later on\n */\n public function syncField(Field $field): void\n {\n try {\n if (FieldHelper::isCustomField($field)) {\n $query = '\n SELECT\n Id, Metadata, TableEnumOrId\n FROM\n CustomField\n WHERE\n DeveloperName = :fieldName\n AND\n TableEnumOrId = :fieldType\n AND\n NamespacePrefix = :namespacePrefix';\n\n // We need to constrain the field lookup to the object, in case it's used in multiple places.\n $objectType = \\in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true)\n ? 'activity'\n : $field->object_type;\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'fieldName' => substr($field->crm_provider_id, 0, -\\strlen('__c')),\n 'fieldType' => ucfirst($objectType),\n\n // This is used to ensure we only consider the field within the org, not installed packages.\n 'namespacePrefix' => 'null',\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Sync field metadata.\n $metadata = $sfField['Metadata'];\n\n $field->description = mb_strimwidth($metadata['description'] ?? '', 0, 191);\n $field->label = mb_strimwidth($metadata['label'] ?? '', 0, Field::LABEL_MAX_LENGTH);\n $field->type = $this->convertFieldType($metadata['type'], $field->getEntityName());\n $field->is_mandatory = ($metadata['required'] === true);\n $field->length = $metadata['length'];\n $field->default_value = mb_strimwidth(trim($metadata['defaultValue'] ?? '', '\"'), 0, 191);\n $field->save();\n } else {\n $query = '\n SELECT\n Id, DataType, DeveloperName, Label, Length, Description\n FROM\n FieldDefinition\n WHERE\n DurableId = :entityName';\n\n $entityName = $field->getEntityName();\n $sfFields = $this->queryHandler->metadata($query, [\n 'entityName' => $entityName,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n $convertedType = $this->convertFieldType($sfField['DataType'], $entityName);\n $label = mb_strimwidth($sfField['Label'], 0, Field::LABEL_MAX_LENGTH);\n\n if ($field->isBusinessType()) {\n $label = 'Opportunity Type';\n }\n\n $field->description = mb_strimwidth($sfField['Description'], 0, Field::DESCRIPTION_MAX_LENGTH);\n $field->label = $label;\n $field->type = $convertedType;\n $field->length = $sfField['Length'];\n $field->save();\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n private function convertFieldType(string $from, ?string $entityName = null): string\n {\n $converter = new FieldTypeConverter();\n\n return $converter->convert($from, $entityName);\n }\n\n /**\n * @inheritdoc\n */\n public function importPicklistValues(Field $field): array\n {\n $values = [];\n $fieldValues = [];\n\n try {\n if (FieldHelper::isCustomField($field)) {\n $query = '\n SELECT\n Id, Metadata, TableEnumOrId\n FROM\n CustomField\n WHERE\n DeveloperName = :fieldName\n AND\n TableEnumOrId = :fieldType\n AND\n NamespacePrefix = :namespacePrefix';\n\n // We need to constrain the field lookup to the object, in case it's used in multiple places.\n $objectType = \\in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true) ?\n 'activity' : $field->object_type;\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'fieldName' => substr($field->crm_provider_id, 0, -\\strlen('__c')),\n 'fieldType' => ucfirst($objectType),\n // This is used to ensure we only consider the field within the org, not installed packages.\n 'namespacePrefix' => 'null',\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n $valueSet = $sfField['Metadata']['valueSet'];\n\n if ($valueSet['valueSetName'] === null) {\n // Local picklist values can be obtained easily.\n $picklistValues = $valueSet['valueSetDefinition']['value'];\n } else {\n // But for some fields, we just get the Global Value Picklist pointer so need to do more work.\n $picklistValues = $this->importGlobalValuePicklistValues($valueSet['valueSetName']);\n }\n\n // Import all active values.\n foreach ($picklistValues as $i => $sfFieldValue) {\n // Setup default value.\n if ($sfFieldValue['default']) {\n $field->update(['default_value' => $sfFieldValue['valueName']]);\n }\n\n // This comes through as null if active (lol).\n if ($sfFieldValue['isActive'] !== false) {\n $values[] = [\n 'value' => $sfFieldValue['valueName'],\n 'label' => $sfFieldValue['valueName'],\n 'sequence' => $i,\n 'is_default' => $sfFieldValue['default'],\n ];\n }\n }\n } else {\n $objectFields = $this->getObjectFields($field->object_type);\n $fieldId = $field->crm_provider_id;\n\n // Only work with our field of interest.\n $objectField = array_filter($objectFields, function ($item) use ($fieldId) {\n return $item['name'] === $fieldId;\n });\n\n $objectField = array_shift($objectField);\n if (empty($objectField['picklistValues']) === false) {\n foreach ($objectField['picklistValues'] as $i => $sfFieldValue) {\n // Skip inactive values.\n if ($sfFieldValue['active'] === false) {\n continue;\n }\n\n // Setup default value.\n if ($sfFieldValue['defaultValue']) {\n $field->update(['default_value' => $sfFieldValue['value']]);\n }\n\n $values[] = [\n 'value' => $sfFieldValue['value'],\n 'label' => $sfFieldValue['label'],\n 'sequence' => $i,\n 'is_default' => $sfFieldValue['defaultValue'],\n ];\n }\n }\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'] ?? '', 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, true)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n // Get IDs of the values to be deleted\n $valuesToDelete = $field->values()->whereIn('value', $fieldsToPurge);\n $valuesToDeleteIds = $valuesToDelete->pluck('id');\n if (! $valuesToDeleteIds->isEmpty()) {\n $recordTypeFieldValuesRepository = app(RecordTypeFieldValuesRepository::class);\n $recordTypeFieldValuesRepository->deleteForCrmFieldValueIds($valuesToDeleteIds->toArray());\n\n // Now safely delete from crm_field_values\n $valuesToDelete->delete();\n }\n\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n return $fieldValues;\n }\n\n /**\n * Gets values from Global Value Picklists.\n */\n private function importGlobalValuePicklistValues(string $picklistName): array\n {\n $query = '\n SELECT\n Metadata\n FROM\n GlobalValueSet\n WHERE\n DeveloperName = :picklistName\n LIMIT 1';\n\n try {\n $sfValues = $this->queryHandler->metadata($query, [\n 'picklistName' => $picklistName,\n ]);\n\n // There is always 1 result at this point.\n $sfValue = $sfValues->current();\n\n return $sfValue['Metadata']['customValue'];\n } catch (NoResultsException $noResultsException) {\n // Nothing returned.\n\n return [];\n }\n }\n\n /**\n * @inheritdoc\n */\n public function syncProfileRecordTypes(): void\n {\n $objectTypes = [\n 'lead',\n 'account',\n 'contact',\n 'opportunity',\n 'task',\n 'event',\n ];\n\n foreach ($objectTypes as $objectType) {\n try {\n $crmRecordTypes = $this->getRecordTypes(ucfirst($objectType));\n\n foreach ($crmRecordTypes as $crmRecordType) {\n // If the record type is default and not the Master type, set this.\n if ($crmRecordType['default'] && $crmRecordType['recordTypeId'] !== '012000000000000AAA') {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $crmRecordType['recordTypeId'])\n ->first();\n\n if ($recordType) {\n $this->profile->{$objectType . '_record_type_id'} = $recordType->id;\n }\n }\n }\n } catch (HttpNotFoundException $exception) {\n Log::error('No access to ' . $objectType . ' object, skipping...');\n\n // XXX: should we log this fact somewhere?\n continue;\n }\n }\n\n if ($this->profile->isDirty()) {\n $this->profile->save();\n }\n }\n\n /**\n * Gets business processes.\n */\n public function importBusinessProcesses(): void\n {\n $query = '\n SELECT\n Id, IsActive, Name, TableEnumOrId\n FROM\n BusinessProcess\n WHERE\n TableEnumOrId IN (\\'Lead\\',\\'Opportunity\\')';\n\n try {\n $sfProcesses = $this->queryHandler->query($query);\n\n // Upsert all processes for the team.\n foreach ($sfProcesses as $sfProcess) {\n /** @var BusinessProcess $businessProcess */\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $sfProcess['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => $sfProcess['Name'],\n 'type' => $sfProcess['TableEnumOrId'] === 'Lead' ? 'lead' : 'opportunity',\n 'is_selectable' => $sfProcess['IsActive'],\n ]);\n\n $this->importBusinessProcessStages($businessProcess);\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * Gets business process stages.\n */\n private function importBusinessProcessStages(BusinessProcess $businessProcess): void\n {\n $query = '\n SELECT\n Metadata\n FROM\n BusinessProcess\n WHERE\n Id = :processId';\n\n try {\n $stages = [];\n $sfProcessStages = $this->queryHandler->metadata($query, [\n 'processId' => $businessProcess->crm_provider_id,\n ]);\n\n // There is always 1 result at this point.\n $sfProcessStage = $sfProcessStages->current();\n\n // Upsert all processes for the team.\n foreach ($sfProcessStage['Metadata']['values'] as $sfProcessStage) {\n $sanitizedName = urldecode($sfProcessStage['valueName']); // Must decode: \"%2C\" becomes \",\" etc.\n\n $stage = $businessProcess->crm->stages()\n // This MUST match on label because this API doesn't use API Name.\n ->where('label', $sanitizedName)\n ->where('type', $businessProcess->type)\n ->where('is_selectable', 1)\n ->first();\n\n if ($stage) {\n $stages[] = $stage->id;\n }\n }\n\n $businessProcess->stages()->sync($stages);\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * Gets record types.\n */\n public function importRecordTypes(): void\n {\n $query = '\n SELECT\n Id, IsActive, Name, BusinessProcessId, SobjectType\n FROM\n RecordType';\n\n try {\n $sfRecordTypes = $this->queryHandler->query($query);\n\n // Upsert all record types for the process.\n foreach ($sfRecordTypes as $sfRecordType) {\n $businessProcess = null;\n if ($sfRecordType['BusinessProcessId']) {\n $businessProcess = $this->config->businessProcesses()\n ->where('crm_provider_id', $sfRecordType['BusinessProcessId'])\n ->first();\n }\n\n /** @var RecordType $recordType */\n $recordType = $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $sfRecordType['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'type' => mb_strtolower($sfRecordType['SobjectType']),\n 'name' => $sfRecordType['Name'],\n 'is_selectable' => $sfRecordType['IsActive'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n $this->importRecordTypeFieldValues($recordType);\n }\n } catch (NoResultsException $noResultsException) {\n // Do nothing.\n }\n }\n\n /**\n * Import record type - field value mappings. This only works for standard fields.\n */\n private function importRecordTypeFieldValues(RecordType $recordType): void\n {\n try {\n $query = '\n SELECT\n Metadata\n FROM\n RecordType\n WHERE\n Id = :recordTypeId';\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'recordTypeId' => $recordType->crm_provider_id,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Sync field metadata.\n $picklists = $sfField['Metadata']['picklistValues'];\n\n foreach ($picklists as $picklist) {\n $field = $this->config->fields()->where([\n 'type' => Field::TYPE_PICKLIST,\n 'object_type' => $recordType->type,\n 'crm_provider_id' => $picklist['picklist'],\n ])->first();\n\n if ($field) {\n $fieldValues = [];\n\n foreach ($picklist['values'] as $value) {\n // Must decode: \"%2C\" becomes \",\" etc.\n $fieldValue = $field->values()\n ->where('value', urldecode($value['valueName']))\n ->first();\n\n if ($fieldValue) {\n $fieldValues[] = $fieldValue->id;\n }\n }\n\n $recordType->fieldValues()->sync($fieldValues);\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $params = [];\n $missingStage = null;\n if ($types === null) {\n $types = [Stage::TYPE_LEAD, Stage::TYPE_OPPORTUNITY];\n }\n\n foreach ($types as $type) {\n if ($type === Stage::TYPE_LEAD) {\n $query = '\n SELECT\n Id, ApiName, MasterLabel, SortOrder\n FROM\n LeadStatus';\n } else {\n $query = '\n SELECT\n Id, ApiName, MasterLabel, IsActive, SortOrder, DefaultProbability\n FROM\n OpportunityStage';\n }\n\n if ($missingStageName) {\n $escapedStageName = ValueNormalizer::replaceQueryWithStringLiterals($missingStageName);\n\n $query .= ' WHERE ApiName = :stageName';\n\n $params = [\n 'stageName' => $escapedStageName,\n ];\n }\n\n try {\n $sfStages = $this->queryHandler->query($query, $params);\n } catch (NoResultsException $exception) {\n $sfStages = [];\n }\n\n $missingStage = null;\n\n // Upsert all stages for the team.\n foreach ($sfStages as $sfStage) {\n $selectable = true;\n if (array_key_exists('IsActive', $sfStage)) {\n $selectable = $sfStage['IsActive'];\n }\n\n $this->restoreAnyTrashedEntity($this->config->stages(), $sfStage['Id']);\n\n $stage = $this->config->stages()->updateOrCreate([\n 'crm_provider_id' => $sfStage['Id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($sfStage['ApiName'], 0, 50),\n 'label' => mb_strimwidth($sfStage['MasterLabel'], 0, 191),\n 'type' => $type,\n 'sequence' => $sfStage['SortOrder'] ?? 0,\n 'is_selectable' => $selectable,\n 'probability' => $sfStage['DefaultProbability'] ?? null,\n ]);\n\n if ($missingStageName && $missingStageName === $sfStage['ApiName']) {\n $missingStage = $stage;\n }\n }\n\n if ($missingStageName && $missingStage === null) {\n // If they requested a stage that still doesn't exist, it must be inactive so lazy create it.\n $missingStage = $this->config->stages()->create([\n 'crm_provider_id' => Uuid::uuid4(),\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($missingStageName, 0, 50),\n 'label' => mb_strimwidth($missingStageName, 0, 191),\n 'type' => $type,\n 'sequence' => 0,\n 'is_selectable' => 0,\n ]);\n }\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('lead');\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Lead\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfLeads = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfLeads as $sfLead) {\n // Only sync if previously imported.\n if ($this->hasLead($sfLead['Id'])) {\n $this->importLead($sfLead);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::LEAD);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n $fields = $this->getAllFieldsAsArray('lead');\n\n $sfLead = $this->getRecord('Lead', $crmId, $fields);\n\n return $this->importLead($sfLead);\n }\n\n private function importLead($crmData): ?Lead\n {\n /** @var ?Stage $stage */\n $stage = null;\n if (isset($crmData['Status'])) {\n // Get the current stage.\n $stage = $this->config\n ->stages()\n ->where('name', $crmData['Status'])\n ->where('type', Stage::TYPE_LEAD)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_LEAD], $crmData['Status']);\n }\n }\n\n // If we have no way of importing this, just return null :(\n if ($stage === null) {\n return null;\n }\n\n $countryCode = $crmData['CountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country name.\n if ($countryCode === null && empty($crmData['Country']) !== false) {\n $countryCode = $this->convertCountryNameToCode($crmData['Country']);\n }\n\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['Phone'] ?? '', 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n\n $mobilePhone = null;\n if (empty($crmData['MobilePhone']) === false) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['MobilePhone'], 0, 25);\n $mobilePhone = phone_e164($countryCode, $number);\n }\n\n $convertedDate = null;\n $convertedAccount = null;\n $convertedOpportunity = null;\n $convertedContact = null;\n\n if ($crmData['IsConverted'] == 'true') {\n $convertedDate = $crmData['ConvertedDate'];\n\n if (empty($crmData['ConvertedAccountId']) === false) {\n $convertedAccount = $this->config\n ->accounts()\n ->where('crm_provider_id', $crmData['ConvertedAccountId'])\n ->first();\n\n if ($convertedAccount === null) {\n try {\n $convertedAccount = $this->syncAccount($crmData['ConvertedAccountId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n\n if (empty($crmData['ConvertedOpportunityId']) === false) {\n $convertedOpportunity = $this->config\n ->opportunities()\n ->where('crm_provider_id', $crmData['ConvertedOpportunityId'])\n ->first();\n\n if ($convertedOpportunity === null) {\n try {\n $convertedOpportunity = $this->syncOpportunity($crmData['ConvertedOpportunityId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n\n if (empty($crmData['ConvertedContactId']) === false) {\n $convertedContact = $this->team\n ->crm\n ->contacts()\n ->where('crm_provider_id', $crmData['ConvertedContactId'])\n ->first();\n\n if ($convertedContact === null) {\n try {\n $convertedContact = $this->syncContact($crmData['ConvertedContactId']);\n } catch (HttpNotFoundException $exception) {\n // Probably the user has no permissions to access the converted data.\n }\n }\n }\n }\n\n if (empty($crmData['Company'])) {\n $company = 'Unknown';\n } else {\n $company = mb_strimwidth($crmData['Company'], 0, 191);\n }\n\n $domain = null;\n if (empty($crmData['Website']) === false) {\n $domain = mb_strimwidth($crmData['Website'], 0, 191);\n $domain = StringUtil::resolveDomain($domain);\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'] ?? '',\n 'company' => $company,\n 'domain' => $domain,\n 'name' => $crmData['Name'] ? mb_strimwidth($crmData['Name'], 0, 191) : '',\n 'title' => $crmData['Title'] ? mb_strimwidth($crmData['Title'], 0, 128) : null,\n 'email' => $crmData['Email'] ? mb_strimwidth($crmData['Email'], 0, 80) : null,\n 'phone' => $parsedNumber['phone'],\n 'ext' => $parsedNumber['ext'] ?? null,\n 'mobile_phone' => $mobilePhone,\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Lead::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'stage_id' => $stage->id,\n 'record_type_id' => null,\n 'converted_at' => $convertedDate,\n 'converted_account_id' => $convertedAccount->id ?? null,\n 'converted_opportunity_id' => $convertedOpportunity->id ?? null,\n 'converted_contact_id' => $convertedContact->id ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->leads(), $crmData['Id']);\n\n /** @var Lead $lead */\n $lead = $this->config->leads()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n if ($lead->wasChanged('converted_at') && $lead->getConvertedAt() !== null) {\n $this->eventDispatcher->dispatch(new LeadConverted($lead));\n }\n\n $this->handleObjectDeletion($lead, $crmData);\n\n if ($lead->trashed()) {\n return null;\n }\n\n return $lead;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('account');\n\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Account\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfAccounts = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfAccounts as $sfAccount) {\n // Only sync if previously imported.\n if ($this->hasAccount($sfAccount['Id'])) {\n $this->importAccount($sfAccount);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::ACCOUNT);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n $fields = $this->getAllFieldsAsArray('account');\n if (! in_array('Id', $fields, true)) {\n $this->logger->info('[Salesforce] Sync account cancelled. Fields are not available.', [\n 'crmId' => $crmId,\n 'userId' => $this->profile->getUserId(),\n ]);\n\n return null;\n }\n\n $sfAccount = $this->getRecord('Account', $crmId, $fields);\n\n return $this->importAccount($sfAccount);\n }\n\n private function importAccount($crmData): ?Account\n {\n if (! empty($crmData['IsDeleted'])) {\n $this->handleEntityDeletionByProviderId($this->config->accounts(), $crmData);\n\n return null;\n }\n\n $countryCode = $crmData['BillingCountryCode'] ?? $crmData['ShippingCountryCode'] ?? null;\n\n // Salesforce allows custom \"countries\" to be created. Disregard these.\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n // If we have no country code, try to parse it from the country names.\n if ($countryCode === null && empty($crmData['BillingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['BillingCountry']);\n }\n\n if ($countryCode === null && empty($crmData['ShippingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['ShippingCountry']);\n }\n\n if (empty($crmData['Phone']) === false) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($crmData['Phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $industry = null;\n if (empty($crmData['Industry']) === false) {\n $industry = mb_strimwidth($crmData['Industry'], 0, 40);\n }\n\n $domain = null;\n if (empty($crmData['Website']) === false) {\n $domain = mb_strimwidth($crmData['Website'], 0, 191);\n $domain = StringUtil::resolveDomain($domain);\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'],\n 'name' => mb_strimwidth($crmData['Name'], 0, 191),\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Account::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'industry' => $industry,\n 'domain' => $domain,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->accounts(), $crmData['Id']);\n\n /** @var Account $account */\n $account = $this->config->accounts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n $this->handleObjectDeletion($account, $crmData);\n\n return $account->trashed() ? null : $account;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOpportunities(array $parameters, ?string $strategy = null): int\n {\n $strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);\n\n $syncCount = 0;\n $logParams = $parameters;\n $parameters['profile'] = $this->profile;\n $logParams['user'] = $this->profile->getUserId();\n\n if (count($strategies) > 1) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Multiple sync strategies used', [\n 'teamId' => $this->team->getUuid(),\n 'params' => $logParams,\n 'strategies_count' => count($strategies),\n ]);\n }\n\n foreach ($strategies as $syncStrategy) {\n $name = $syncStrategy->getStrategyName();\n\n try {\n $sfOpportunities = $syncStrategy->fetchOpportunities($parameters);\n $totalRecords = $sfOpportunities->count();\n\n foreach ($sfOpportunities as $sfOpportunity) {\n $this->importOpportunity($sfOpportunity);\n $syncCount++;\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n $this->logger->warning('[' . $this->getDisplayName() . '] No opportunities found', [\n 'teamId' => $this->team->getUuid(),\n 'name' => $name,\n 'params' => $logParams,\n 'reason' => $noResultsException->getMessage(),\n ]);\n } catch (CrmException $crmException) {\n // Nothing to sync.\n $this->logger->warning('[' . $this->getDisplayName() . '] Opportunity sync failed', [\n 'teamId' => $this->team->getUuid(),\n 'name' => $name,\n 'params' => $logParams,\n 'reason' => $crmException->getMessage(),\n ]);\n }\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::OPPORTUNITY, ['params' => $logParams]);\n\n // debug to see how if count of opportunities reaches 1000\n if ($syncCount >= 1000) {\n $this->logger->info(\n '[' . $this->getDisplayName() . '] Sync Opportunities - count warning',\n [\n 'team_id' => $this->team->getId(),\n 'params' => $logParams,\n 'count' => $syncCount,\n 'strategies_count' => count($strategies),\n 'total_records' => $totalRecords ?? null,\n ]\n );\n }\n\n return $syncCount;\n }\n\n\n /**\n * @inheritdoc\n */\n public function syncOpportunity(string $crmId): ?Opportunity\n {\n $strategy = $this->opportunitySyncStrategyResolver->resolve(\n $this->config,\n OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY\n );\n\n $parameters = [\n 'profile' => $this->profile,\n 'crm_id' => $crmId,\n ];\n\n try {\n $sfOpportunity = $strategy->fetchOpportunities($parameters);\n } catch (HttpNotFoundException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [\n 'teamId' => $this->team->id_string,\n 'crmId' => $crmId,\n ]);\n\n return null;\n } catch (CrmException $crmException) {\n $this->logger->info('[' . $this->getDisplayName() . '] Opportunity sync failed', [\n 'teamId' => $this->team->id_string,\n 'crmId' => $crmId,\n 'exception' => $crmException->getMessage(),\n ]);\n\n return null;\n }\n\n if ($sfOpportunity instanceof ArrayIterator) {\n return $this->importOpportunity($sfOpportunity->getItems());\n }\n\n return $this->importOpportunity($sfOpportunity);\n }\n\n /**\n * @throws HttpNotFoundException\n */\n private function importOpportunity($crmData): ?Opportunity\n {\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $account = null;\n if (empty($crmData['AccountId']) === false) {\n /** @var ?Account $account */\n $account = $this->config->accounts()\n ->where('crm_provider_id', (string) $crmData['AccountId'])\n ->first();\n\n if ($account === null) {\n $account = $this->syncAccount($crmData['AccountId']);\n }\n }\n\n $userId = $profile?->getUserId() ?? $account?->getUserId();\n if ($userId === null) {\n $this->logger->error('[Salesforce] | Skip import, no user_id found', [\n 'id' => $crmData['Id'],\n ]);\n\n return null;\n }\n\n /** @var ?Stage $stage */\n $stage = null;\n if (isset($crmData['StageName'])) {\n $stage = $this->config\n ->stages()\n ->where('name', $crmData['StageName'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->orderBy('is_selectable', 'DESC')\n ->orderBy('id')\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $crmData['StageName']);\n }\n }\n\n $recordType = null;\n if (empty($crmData['RecordTypeId']) === false) {\n /** @var ?RecordType $recordType */\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $crmData['RecordTypeId'])\n ->first();\n }\n\n $createdDate = null;\n if (empty($crmData['CreatedDate']) === false) {\n $createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n }\n\n $closeDate = null;\n if (empty($crmData['CloseDate']) === false) {\n $closeDate = Carbon::parse($crmData['CloseDate'])->format('Y-m-d');\n }\n\n $valueFieldName = 'Amount';\n if ($this->config->opportunity_value_field_id) {\n $valueFieldName = $this->config->opportunityValueField->crm_provider_id;\n }\n\n $data = [\n 'team_id' => $this->team->id,\n 'account_id' => $account->id ?? null,\n 'user_id' => $userId,\n 'owner_id' => $crmData['OwnerId'] ?? null,\n 'name' => mb_strimwidth($crmData['Name'] ?? '', 0, 128),\n 'value' => $crmData[$valueFieldName],\n 'currency_code' => CurrencyFormatter::formatCode($crmData['CurrencyIsoCode'] ?? null),\n 'close_date' => $closeDate,\n 'is_closed' => $crmData['IsClosed'],\n 'is_won' => $crmData['IsWon'],\n 'stage_id' => $stage?->id ?? null,\n 'record_type_id' => $recordType->id ?? null,\n 'remotely_created_at' => $createdDate,\n 'probability' => $crmData['Probability'] ?? null,\n 'forecast_category' => $crmData['ForecastCategoryName'] ?? null,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->opportunities(), $crmData['Id']);\n\n // Do not allow locked DB tables & other errors\n // to interrupt the process of reverting the trashed opportunities\n try {\n /** @var Opportunity $opportunity */\n $opportunity = $this->config->opportunities()\n ->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n // import external fields into crm_field_data if present\n $crmFields = $this->getOpportunitySyncableFields();\n\n $this->importOpportunityCrmFieldData($crmData, $crmFields, $opportunity->id);\n\n $this->handleObjectDeletion($opportunity, $crmData);\n\n if ($opportunity->trashed()) {\n return null;\n }\n\n if ($opportunity->wasRecentlyCreated) {\n MatchActivitiesToNewOpportunity::dispatch($opportunity->getId());\n }\n\n return $opportunity;\n } catch (Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->error('[Salesforce] importOpportunity failure.', [\n 'crm_provider_id' => $crmData['Id'],\n 'team_id' => $this->team->id,\n 'exception' => $exception->getMessage(),\n ]);\n\n $this->handleEntityDeletionByProviderId($this->config->opportunities(), $crmData);\n }\n\n return null;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n $fields = $this->getAllFieldsAsArray('contact');\n if (\\in_array('Id', $fields, true) === false) {\n return $syncCount;\n }\n\n $query = '\n SELECT ' . rtrim(implode(',', $fields), ',') . '\n FROM Contact\n WHERE LastModifiedDate > :since\n ORDER BY LastModifiedDate ASC';\n\n try {\n $sfContacts = $this->queryHandler->query($query, [\n 'since' => $since->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($sfContacts as $sfContact) {\n // Only sync if previously imported.\n if ($this->hasContact($sfContact['Id'])) {\n $this->importContact($sfContact);\n $syncCount++;\n }\n }\n } catch (NoResultsException $noResultsException) {\n // Nothing to sync.\n }\n\n $this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::CONTACT);\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n $fields = $this->getAllFieldsAsArray('contact');\n if (! in_array('Id', $fields, true)) {\n $this->logger->info('[Salesforce] Sync contact cancelled. Fields are not available.', [\n 'crmId' => $crmId,\n 'userId' => $this->profile->getUserId(),\n ]);\n\n return null;\n }\n\n $sfContact = $this->getRecord('Contact', $crmId, $fields);\n\n return $this->importContact($sfContact);\n }\n\n private function importContact($crmData): ?Contact\n {\n if (! empty($crmData['IsDeleted'])) {\n $this->handleEntityDeletionByProviderId($this->config->contacts(), $crmData);\n\n return null;\n }\n\n $account = $this->resolveContactAccount($crmData);\n $countryCode = $this->resolveContactCountryCode($crmData, $account);\n [$parsedNumber, $ext] = $this->parseContactPhone($countryCode, $crmData);\n $mobileNumber = $this->parseContactMobile($countryCode, $crmData);\n $createdDate = empty($crmData['CreatedDate'])\n ? null\n : Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');\n $profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);\n\n $data = [\n 'team_id' => $this->team->id,\n 'account_id' => $account->id ?? null,\n 'user_id' => $profile?->user_id,\n 'owner_id' => $crmData['OwnerId'] ?? null,\n 'name' => ($crmData['Name'] ?? null) !== null ? mb_strimwidth($crmData['Name'], 0, 100) : '',\n 'title' => ($crmData['Title'] ?? null) !== null ? mb_strimwidth($crmData['Title'], 0, 128) : null,\n 'email' => ($crmData['Email'] ?? null) !== null ? mb_strimwidth($crmData['Email'], 0, 191) : null,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $ext,\n 'mobile_phone' => $mobileNumber,\n 'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n crmConfiguration: $this->config,\n crmProviderId: $crmData['Id'],\n modelType: Contact::class,\n fileName: $crmData['Id'],\n avatarText: $crmData['Name']\n ),\n 'remotely_created_at' => $createdDate,\n ];\n\n $this->restoreAnyTrashedEntity($this->config->contacts(), $crmData['Id']);\n\n /** @var Contact $contact */\n $contact = $this->config->contacts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);\n\n $this->handleObjectDeletion($contact, $crmData);\n\n return $contact->trashed() ? null : $contact;\n }\n\n private function resolveContactAccount(array $crmData): ?Account\n {\n if (! isset($crmData['AccountId'])) {\n return null;\n }\n\n return $this->config->accounts()\n ->where('crm_provider_id', (string) $crmData['AccountId'])\n ->first() ?? $this->syncAccount($crmData['AccountId']);\n }\n\n private function resolveContactCountryCode(array $crmData, ?Account $account): ?string\n {\n $countryCode = $crmData['MailingCountryCode'] ?? null;\n\n if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {\n $countryCode = null;\n }\n\n if ($countryCode === null && empty($crmData['MailingCountry']) === false) {\n $countryCode = $this->convertCountryNameToCode($crmData['MailingCountry'])\n ?? ($account?->country_code);\n }\n\n return $countryCode;\n }\n\n private function parseContactPhone(?string $countryCode, array $crmData): array\n {\n if (empty($crmData['Phone'])) {\n return [[], null];\n }\n\n $parsedNumber = parsePhoneNumber($countryCode, Str::limit($crmData['Phone'], 25, ''));\n $ext = empty($parsedNumber['ext']) ? null : Str::limit($parsedNumber['ext'], 10, '');\n\n return [$parsedNumber, $ext];\n }\n\n private function parseContactMobile(?string $countryCode, array $crmData): ?string\n {\n if (empty($crmData['MobilePhone'])) {\n return null;\n }\n\n return Str::limit(phone_e164($countryCode, $crmData['MobilePhone']), 25, '');\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n $fields = [\n 'InstanceName',\n 'OrganizationType',\n 'IsSandbox',\n ];\n\n $orgValues = $this->getRecord('Organization', $this->config->crm_provider_id, $fields);\n\n $edition = null;\n switch ($orgValues['OrganizationType']) {\n case 'Developer Edition':\n $edition = Configuration::EDITION_DEVELOPER;\n\n break;\n\n case 'Professional Edition':\n $edition = Configuration::EDITION_PROFESSIONAL;\n\n break;\n\n case 'Enterprise Edition':\n $edition = Configuration::EDITION_ENTERPRISE;\n\n break;\n }\n\n $this->config->edition = $edition;\n $this->config->instance = $orgValues['InstanceName'];\n\n // XXX: How can this state be possible?\n if ($this->config->version === null) {\n $this->config->version = Client::MIN_API_VERSION;\n }\n\n $installedVersion = $this->getInstalledAppVersion();\n if ($installedVersion !== null) {\n $installedVersion = (string) $this->getInstalledAppVersion();\n }\n\n $this->config->installed_app_version = $installedVersion;\n\n $this->config->save();\n }\n\n public function getInstalledAppVersion(): ?string\n {\n try {\n $query = '\n SELECT\n SubscriberPackageVersion.MajorVersion,\n SubscriberPackageVersion.MinorVersion,\n SubscriberPackageVersion.PatchVersion,\n SubscriberPackageVersion.BuildNumber\n FROM\n InstalledSubscriberPackage\n WHERE\n SubscriberPackageId = :packageId\n ';\n\n $sfFields = $this->queryHandler->metadata($query, [\n 'packageId' => self::INSTALLED_PACKAGE_ID,\n ]);\n\n // There is always 1 result at this point.\n $sfField = $sfFields->current();\n\n // Grab version number.\n $version = $sfField['SubscriberPackageVersion']['MajorVersion'] .\n $sfField['SubscriberPackageVersion']['MinorVersion'] .\n $sfField['SubscriberPackageVersion']['PatchVersion'] .\n $sfField['SubscriberPackageVersion']['BuildNumber'];\n } catch (\\Exception) {\n $version = null;\n }\n\n return $version;\n }\n\n /**\n * Store transcripts as note.\n *\n * @throws \\Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For SF we also check if Log Notes is enabled.\n if ($this->profile->log_notes === Profile::LOG_NOTE_NONE) {\n return;\n }\n\n if ($activity->opportunity_id && $activity->prospect === null) {\n return;\n }\n\n try {\n $transcriptionData = $this->generateTranscription($activity);\n\n $noteMaxLength = $this->profile->log_notes === Profile::LOG_NOTE_ENHANCED\n ? self::ENHANCED_NOTE_MAX_LENGTH\n : self::CLASSIC_NOTE_MAX_LENGTH;\n\n $title = 'Transcript for ';\n $title .= $activity->title ?? $activity->activity_title;\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($transcriptionData, 0, $noteMaxLength);\n\n if ($activity->opportunity_id) {\n $objectId = $activity->opportunity->crm_provider_id;\n } else {\n $objectId = $activity->prospect->crm_provider_id;\n }\n\n $noteId = $this->saveNote($title, $body, $objectId);\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (\\Exception $e) {\n \\Sentry::captureException($e);\n }\n }\n\n public function saveNote(string $title, string $body, string $objectId, ?NoteObject $noteObject = null): ?string\n {\n $noteId = null;\n\n try {\n if ($this->profile->log_notes === Profile::LOG_NOTE_ENHANCED) {\n $noteId = $this->buildEnhancedNote($title, $body, $objectId);\n } else {\n $noteId = $this->buildClassicNote($title, $body, $objectId);\n }\n } catch (HttpNotFoundException $exception) {\n // The profile not having access to create Enhanced Notes. Set their preference to Classic.\n if ($this->profile->log_notes === Profile::LOG_NOTE_ENHANCED) {\n $this->profile->update([\n 'log_notes' => Profile::LOG_NOTE_CLASSIC,\n ]);\n }\n }\n\n return $noteId;\n }\n\n /**\n * This is using the \"Enhanced\" Notes feature, NOT the \"Notes & Attachments\" feature being deprecated.\n *\n * @url https://salesforce.stackexchange.com/questions/104408/how-can-i-create-an-account-note-or-contact-note-via-api-that-is-visible-in-sale\n */\n private function buildEnhancedNote(string $title, string $body, string $objectId): string\n {\n // Decode stored entities, escape HTML (without quoting), then convert line breaks for Salesforce formatting\n $decodedBody = html_entity_decode($body, ENT_QUOTES | ENT_HTML5);\n $sanitizedBody = htmlspecialchars($decodedBody, ENT_NOQUOTES, 'UTF-8', false);\n $content = nl2br($sanitizedBody, false);\n $note = [\n 'OwnerId' => $this->profile->crm_provider_id,\n 'Title' => $title,\n 'Content' => base64_encode($content),\n ];\n\n $noteId = $this->createRecord('ContentNote', $note);\n\n $link = [\n 'ContentDocumentId' => $noteId,\n 'LinkedEntityId' => $objectId,\n 'ShareType' => 'I',\n ];\n\n $this->createRecord('ContentDocumentLink', $link);\n\n return $noteId;\n }\n\n private function buildClassicNote(string $title, string $body, string $objectId): string\n {\n if (in_array($this->parseObjectType($objectId), [Field::OBJECT_TASK, Field::OBJECT_EVENT])) {\n $this->logger->info('[Salesforce] Summary not sent', [\n 'profile_id' => $this->profile->id,\n 'objectId' => $objectId,\n 'reason' => 'Classical Note does not support Task/Event relation',\n ]);\n\n return '';\n }\n\n $titleTrimmed = null;\n\n if (mb_strlen($title) > 80) {\n $titleTrimmed = substr($title, 0, 77) . '...';\n }\n $payload = [\n 'OwnerId' => $this->profile->crm_provider_id,\n 'IsPrivate' => false,\n 'Title' => $titleTrimmed ?? $title,\n 'Body' => $titleTrimmed ? $title . PHP_EOL . $body : $body,\n 'ParentId' => $objectId,\n ];\n\n return $this->createRecord('Note', $payload);\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n if ($this->profile === null) {\n return [];\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $limitValues = ['limit' => $this->limit, 'offset' => $this->offset];\n $sosl = $queryBuilder->buildFindQuery($name, $scopes, $limitValues);\n\n $this->logger->info('[Salesforce] Find prospects', [\n 'profile_id' => $this->profile->id,\n 'sosl_query' => $sosl,\n 'search_string' => $name,\n 'scopes' => $scopes,\n ]);\n\n $data = Cache::remember($this->profile->id . $sosl, self::CACHE_TTL, function () use ($sosl) {\n $data = [];\n\n try {\n // Hit remote API.\n $objects = $this->queryHandler->search($sosl);\n\n // Build mapped list.\n foreach ($objects as $object) {\n $type = strtolower($object['attributes']['type']);\n\n $record = [\n 'crmId' => $object['Id'],\n 'name' => $object['Name'],\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n 'crmUrl' => $this->generateProviderUrl($object['Id'], $type),\n ];\n\n switch ($type) {\n case 'lead':\n if (empty($object['Company']) === false) {\n $record['organization'] = $object['Company'];\n }\n\n if (empty($object['Title']) === false) {\n $record['title'] = $object['Title'];\n }\n\n $stage = $this->config->stages()\n ->where('type', Stage::TYPE_LEAD)\n ->where('name', $object['Status'])\n ->first();\n\n // Lazy create the stage.\n if ($stage === null) {\n $stage = $this->importStages([Stage::TYPE_LEAD], $object['Status']);\n }\n\n if ($stage) {\n $record += [\n 'stage' => [\n 'id' => $stage->id_string,\n 'name' => $stage->name,\n ],\n ];\n }\n\n if (empty($object['RecordTypeId']) === false) {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $object['RecordTypeId'])\n ->first();\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n }\n\n break;\n\n case 'account':\n if (empty($object['Industry']) === false) {\n $record['industry'] = $object['Industry'];\n $record['detailsLine'] = $object['Industry'];\n }\n if (! empty($object['PersonEmail'])) {\n $record['detailsLine'] = $object['PersonEmail'];\n }\n\n break;\n\n case 'contact':\n // For contacts, we should try and fetch their account name too.\n if ($object['AccountId']) {\n // Cheaper to get this locally.\n $account = $this->config->accounts()\n ->where('crm_provider_id', $object['AccountId'])\n ->first(['name']);\n\n if ($account) {\n $record['organization'] = $account->name;\n }\n }\n\n if (! empty($object['IsPersonAccount']) && $object['Email']) {\n $record['detailsLine'] = $object['Email'];\n } else {\n if (empty($object['Title']) === false) {\n $record['title'] = $object['Title'];\n }\n }\n\n break;\n }\n\n // Add phone numbers to record.\n if (empty($object['Phone']) === false && $object['Phone']) {\n $record['phoneNumbers'][] = [\n 'number' => $object['Phone'],\n 'nationalFormat' => phone_national($this->profile->user->country_code, $object['Phone']),\n 'type' => 'phone',\n ];\n }\n\n if (empty($object['MobilePhone']) === false && $object['MobilePhone']) {\n $record['phoneNumbers'][] = [\n 'number' => $object['MobilePhone'],\n 'nationalFormat' => phone_national(\n $this->profile->user->country_code,\n $object['MobilePhone']\n ),\n 'type' => 'mobile',\n ];\n }\n\n $data[] = $record;\n }\n } catch (NoResultsException $e) {\n $data = [];\n }\n\n return $data;\n });\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n try {\n // Perhaps their profile has no opportunity permissions.\n if ($this->profile === null || $this->profile->opportunity_fields === null) {\n return $data;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildFindOpportunitiesQuery();\n\n $objects = $this->queryHandler->query($query, ['accountId' => $crmAccountId]);\n\n foreach ($objects as $object) {\n $record = [\n 'crmId' => $object['Id'],\n 'name' => $object['Name'],\n 'won' => $object['IsWon'],\n 'closed' => $object['IsClosed'],\n ];\n\n $valueFieldName = 'Amount';\n if ($this->config->opportunity_value_field_id) {\n $valueFieldName = $this->config->opportunityValueField->crm_provider_id;\n }\n\n if (empty($object[$valueFieldName]) === false) {\n $currency = $object['CurrencyIsoCode'] ?? $this->config->default_currency;\n $value = formatCurrency($object[$valueFieldName], $currency);\n\n $record += [\n 'value' => $value,\n ];\n }\n\n $stage = $this->config->stages()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->where('name', $object['StageName'])\n ->first();\n\n // Lazy create the stage.\n if ($stage === null) {\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $object['StageName']);\n }\n\n $record += [\n 'stage' => [\n 'id' => $stage->id_string,\n 'name' => $stage->name,\n ],\n ];\n\n if (empty($object['RecordTypeId']) === false) {\n $recordType = $this->config->recordTypes()\n ->where('crm_provider_id', $object['RecordTypeId'])\n ->first();\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n }\n\n if ($ownerId && isset($object['OwnerId']) && $object['OwnerId'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n } catch (NoResultsException $e) {\n return $data;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n public function getContactRolesFromCrm(?Carbon $since = null): array\n {\n $roles = [];\n\n if ($this->profile === null) {\n return $roles;\n }\n\n $queryBuilder = app(QueryBuilder::class, ['profile' => $this->profile]);\n\n $query = $queryBuilder->buildGetContactRolesQuery($since);\n\n try {\n $objects = $this->queryHandler->query($query);\n\n foreach ($objects as $object) {\n $roles[] = [\n 'id' => $object['Id'],\n 'contactId' => $object['ContactId'],\n 'opportunityId' => $object['OpportunityId'],\n 'ownerId' => $object['Opportunity']['OwnerId'] ?? null,\n 'isPrimary' => $object['IsPrimary'],\n 'role' => $object['Role'],\n ];\n }\n } catch (NoResultsException) {\n // Just return an empty array.\n $this->logger->info('[Salesforce] No contact roles found', [\n 'since' => $since?->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n }\n\n return $roles;\n }\n\n public function syncContactRoles(Carbon $since): int\n {\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n $crmContactRoles = $this->getContactRolesFromCrm(since: $since);\n $syncCount = 0;\n $contactRoles = [];\n\n foreach ($crmContactRoles as $crmContactRole) {\n $contactRoles[] = $this->importContactRole($crmContactRole);\n $syncCount++;\n }\n\n $contactRoleRepository->saveContactRoles($contactRoles);\n\n $this->syncRemotelyDeletedContactRoles();\n\n return $syncCount;\n }\n\n private function importContactRole(array $contactRole): array\n {\n $contact = $this->config->contacts()\n ->where('crm_provider_id', $contactRole['contactId'])\n ->first();\n\n if ($contact === null) {\n $contact = $this->syncContact($contactRole['contactId']);\n }\n\n $opportunity = $this->config->opportunities()\n ->where('crm_provider_id', $contactRole['opportunityId'])\n ->first();\n\n if ($opportunity === null) {\n $opportunity = $this->syncOpportunity($contactRole['opportunityId']);\n }\n\n $role = null;\n if (! empty($contactRole['role'])) {\n $role = mb_strimwidth($contactRole['role'], 0, 191);\n }\n\n return [\n 'crm_configuration_id' => $this->config->getId(),\n 'contact_id' => $contact->getId(),\n 'crm_provider_id' => $contactRole['id'],\n 'subject_type' => ContactRole::SUBJECT_TYPE_OPPORTUNITY,\n 'subject_id' => $opportunity->getId(),\n 'is_primary' => $contactRole['isPrimary'],\n 'role' => $role,\n ];\n }\n\n protected function syncRemotelyDeletedContactRoles(): bool\n {\n try {\n $deletedRemotely = $this->queryHandler->queryDeleted('OpportunityContactRole');\n } catch (NoResultsException $e) {\n return false;\n }\n\n $deletedOpportunities = $deletedRemotely->getResults();\n $deletedIds = array_column($deletedOpportunities, 'id');\n\n $contactRoleRepository = app(ContactRoleRepository::class);\n\n foreach (array_chunk($deletedIds, self::HARD_DELETE_CHUNK) as $chunk) {\n $contactRoleRepository->deleteContactRoles($chunk);\n\n $this->logger->info('[' . $this->getDisplayName() . '] Remotely deleted opportunities synced', [\n 'teamId' => $this->team->id_string,\n 'remotelyDeletedOpportunities' => $chunk,\n 'count' => count($chunk),\n ]);\n }\n\n return true;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = $objects = [];\n\n $hasWho = \\in_array($objectType, ['lead', 'contact']);\n $playbook = $this->getPlaybook($this->profile->user);\n $fields = array_merge(\n $this->profile->getFieldsAsArray(parent::OBJECT_TASK),\n $playbook && $playbook->activityField ? [$playbook->activityField->crm_provider_id] : []\n );\n\n // Query should default to any open call for that user.\n $query = '\n SELECT ' . implode(',', array_unique($fields)) . '\n FROM Task\n WHERE OwnerId = :ownerId\n AND IsArchived = false\n AND IsDeleted = false\n AND IsClosed = false\n AND (';\n\n if ($objectType === 'account') {\n // This covers tasks tied to a related contact or opportunity too.\n $query .= '\n AccountId = :accountId';\n }\n\n if ($hasWho) {\n $query .= '\n WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($opportunityId) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ' ) ORDER BY LastModifiedDate DESC';\n\n try {\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $this->profile->crm_provider_id,\n 'whoId' => $objectId,\n 'whatId' => $opportunityId,\n 'accountId' => $objectId,\n ]);\n } catch (NoResultsException $e) {\n return $data;\n } finally {\n $this->logger->debug(sprintf('[Salesforce] Found %s tasks for query \"%s\"', count($objects), $query));\n }\n\n foreach ($objects as $object) {\n $dueDate = $object['ActivityDate'] ? Carbon::parse($object['ActivityDate'])->toIso8601String() : null;\n $data[] = [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'due' => $dueDate,\n 'type' => $object[$playbook->activityField->crm_provider_id],\n ];\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getEvents(string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = $objects = [];\n $user = $this->profile?->user;\n if ($this->profile === null || $user === null) {\n return $data;\n }\n\n $hasWho = \\in_array($objectType, ['lead', 'contact']);\n $playbook = $this->getPlaybook($user);\n $fields = array_merge(\n $this->profile->getFieldsAsArray(parent::OBJECT_EVENT),\n $playbook && $playbook->activityField ? [$playbook->activityField->crm_provider_id] : []\n );\n\n // Query should default to any event starting in the last week and ending up until today owned by the user.\n $query = '\n SELECT ' . implode(',', array_unique($fields)) . '\n FROM Event\n WHERE OwnerId = :ownerId\n AND IsArchived = false\n AND IsAllDayEvent = false\n AND StartDateTime >= LAST_N_DAYS:7\n AND EndDateTime <= TODAY\n AND (';\n\n if ($objectType === 'account') {\n // This covers events tied to a related contact or opportunity too.\n $query .= '\n AccountId = :accountId';\n }\n\n if ($hasWho) {\n $query .= '\n WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($opportunityId) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ' ) ORDER BY LastModifiedDate DESC';\n\n try {\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $this->profile->crm_provider_id,\n 'whoId' => $objectId,\n 'whatId' => $opportunityId,\n 'accountId' => $objectId,\n ]);\n } catch (NoResultsException $e) {\n return $data;\n } finally {\n $this->logger->debug(sprintf('[Salesforce] Found %s tasks for query \"%s\"', count($objects), $query));\n }\n\n foreach ($objects as $object) {\n $dueDate = $object['StartDateTime'] ? Carbon::parse($object['StartDateTime'])->toIso8601String() : null;\n\n $data[] = [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'due' => $dueDate,\n 'type' => $object[$playbook->activityField->crm_provider_id],\n ];\n }\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByQuery($email, Field::TYPE_EMAIL);\n if ($sosl === null) {\n return null;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $email,\n QueryHandler::PRIORITISE_EMAIL\n );\n\n $data = $this->convertCrmData($objects, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (NoResultsException $e) {\n // Try the account next.\n if ($this->profile->account_fields === null) {\n return null;\n }\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n // SF improved search - strip the domain extension, min domain name length 4\n return $this->getCompanyNameFromEmail(email: $email, minNameLength: 4);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByDomainQuery($companyName);\n\n try {\n $objects = $this->queryHandler->search($sosl);\n\n $data = $this->convertCrmData($objects, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (NoResultsException) {\n return null;\n }\n }\n\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n // Don't bother looking up numbers that are masked.\n if (str_contains($phone, '**')) {\n return null;\n }\n\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $phoneNational = phone_national(null, $phone) ?? '';\n $possiblePhoneFormats = collect([\n preg_replace('/\\D/', '', ltrim($phone, '0+')),\n preg_replace('/\\D/', '', $phoneNational),\n formatDashPhoneNumber($phone),\n $phoneNational,\n ])\n ->filter() // Removes null and empty strings\n ->unique()\n ->values();\n\n foreach ($possiblePhoneFormats as $phone) {\n $sosl = $queryBuilder->buildMatchByQuery($phone, Field::TYPE_PHONE);\n if ($sosl === null) {\n continue;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $phone,\n QueryHandler::PRIORITISE_PHONE\n );\n\n return $this->convertCrmData($objects, $userId);\n } catch (NoResultsException) {\n continue;\n }\n }\n\n return null;\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->profile->id . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /** Determine the CRM Objects which represent the call activity. */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n if ($this->profile === null) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $sosl = $queryBuilder->buildMatchByQuery($name, 'name');\n if ($sosl === null) {\n return false;\n }\n\n try {\n $objects = $this->queryHandler->search($sosl);\n } catch (NoResultsException $e) {\n return false;\n }\n\n $objects = $this->queryHandler->prioritiseResults(\n $objects,\n $name,\n QueryHandler::PRIORITISE_NAME\n );\n\n $data = $this->convertCrmData($objects, $userId);\n\n return (! empty(array_filter($data))) ? $data : false;\n });\n\n return is_array($result) ? $result : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(QueryIterator $objects, ?int $userId = null): array\n {\n $lead = null;\n $contact = null;\n $opportunity = null;\n $account = null;\n $stage = null;\n $countryCode = null;\n\n if ($objects->count() > 0) {\n $object = $objects->current();\n\n if ($object['attributes']['type'] === 'Lead') {\n $lead = $this->importLead($object);\n\n // Lead might not be imported if the Stage is null for example.\n if ($lead) {\n $countryCode = $lead->country_code;\n $stage = $lead->stage;\n }\n } else {\n if ($object['attributes']['type'] === 'Contact') {\n $contact = $this->importContact($object);\n $account = $contact?->account;\n } else {\n $account = $this->importAccount($object);\n }\n\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account) {\n $countryCode = $account->country_code;\n }\n\n try {\n $sfOpportunities = $this->findOpportunities(\n $account?->getCrmProviderId(),\n $contact?->getCrmProviderId(),\n $userId\n );\n\n // Take the first opportunity, which will be ordered as priority based on their settings.\n if (! empty($sfOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($sfOpportunities[0]['crmId']);\n $stage = $opportunity?->stage;\n }\n } catch (Exception) {\n // Nothing to see here.\n }\n }\n }\n\n return [\n $lead,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n /**\n * @inheritdoc\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n if ($stage->type === Stage::TYPE_LEAD) {\n $objectType = 'Lead';\n $objectId = $crmObject->crm_provider_id;\n $objectStageType = 'Status';\n } else {\n $objectType = 'Opportunity';\n $objectId = $crmObject->crm_provider_id;\n $objectStageType = 'StageName';\n }\n\n $headers = [];\n if ($this->config->trigger_assignment_rules === false) {\n // @see: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/headers_autoassign.htm\n $headers = [\n 'Sforce-Auto-Assign' => 'false',\n ];\n }\n\n $this->updateRecord($objectType, $objectId, [$objectStageType => $stage->name], $headers);\n }\n\n public function parseObjectType(string $objectId): string\n {\n if (Str::startsWith($objectId, '001')) {\n return 'account';\n }\n\n if (Str::startsWith($objectId, '003')) {\n return 'contact';\n }\n\n if (Str::startsWith($objectId, '00Q')) {\n return 'lead';\n }\n\n if (Str::startsWith($objectId, '006')) {\n return 'opportunity';\n }\n\n if (Str::startsWith($objectId, '00U')) {\n return 'event';\n }\n\n if (Str::startsWith($objectId, '00T')) {\n return 'task';\n }\n\n throw new \\InvalidArgumentException('Unsupported Object Type');\n }\n\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, ['profile' => $this->profile]);\n $query = $queryBuilder->buildGetUsersQuery($userToSearch);\n\n try {\n $salesforceUsers = $this->queryHandler->query($query, [\n 'active' => true,\n ]);\n } catch (NoResultsException $e) {\n $this->logger->info('[Salesforce] Sync Profiles. No users found', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $teamRepository = app(TeamRepository::class);\n $customRules = $this->getCustomProfileRules($teamRepository);\n\n foreach ($salesforceUsers as $crmUser) {\n if ($crmUser['Email'] === null) {\n continue;\n }\n\n if (! $this->customProfileValidation($crmUser, $customRules)) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $crmUser['Email']);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $edition = $crmUser['UserPreferencesLightningExperiencePreferred']\n ? Profile::EDITION_LIGHTNING\n : Profile::EDITION_CLASSIC;\n\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->updateOrCreateProfile(\n $user,\n [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $crmUser['Id'],\n ],\n [\n 'user_id' => $user->getId(),\n 'edition' => $edition,\n 'has_external_cti' => ! empty($crmUser['CallCenterId']),\n 'crm_profile_id' => $crmUser['ProfileId'],\n ]\n );\n\n if ($userToSearch instanceof User && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n // Clean up inactive profiles\n try {\n $this->archiveInactiveProfiles();\n } catch (\\Exception $e) {\n $this->logger->warning('[Salesforce] Profile archiving failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n\n // For Salesforce it's easy, we just point every object to the apex domain and they handle it.\n switch ($objectType) {\n case 'lead':\n case 'account':\n case 'contact':\n case 'opportunity':\n case 'task':\n case 'event':\n case 'activity':\n\n $url = $this->config->crm_base_url . '/' . $providerId;\n\n break;\n }\n\n return $url;\n }\n\n public function buildTaskSearchFields(): array\n {\n return ['Id', 'WhoId', 'WhatId', 'AccountId'];\n }\n\n public function getTaskByFilterConditions(\n array $fields,\n array $filters,\n bool $bulkSearch = false,\n bool $strictFilters = true\n ): ?array {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildSearchTaskQuery($fields, $filters, $bulkSearch, $strictFilters);\n\n try {\n if (! $bulkSearch) {\n $objects = $this->queryHandler->query($query, $filters);\n if ($objects->count() === 1) {\n return $objects->current();\n }\n }\n\n if ($bulkSearch) {\n $objects = $this->queryHandler->query($query);\n $records = [];\n foreach ($objects as $record) {\n $key = $record[end($fields)];\n $records[$key] = $record;\n }\n\n return $records;\n }\n } catch (\\Exception $e) {\n $this->logger->info('[Salesforce] Failed to execute query', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function mapCrmObjects(array $task): array\n {\n $activityData = [];\n\n if (! empty($task['WhoId'])) {\n $type = $this->parseObjectType($task['WhoId']);\n $activityData[$type] = $task['WhoId'];\n }\n if (! empty($task['AccountId'])) {\n $activityData['account'] = $task['AccountId'];\n }\n if (! empty($task['WhatId'])) {\n $activityData['opportunity'] = $task['WhatId'];\n }\n\n return $activityData;\n }\n\n /**\n * Get SF task by Outreach call id.\n */\n public function getTaskByFilter(\n string $activityFieldType,\n array $filters,\n string $operator = '=',\n array $additionalFields = []\n ): ?array {\n $data = [];\n\n try {\n // Default (base) fields.\n $fields = ['Id', 'Subject', 'Description', 'ActivityDate', 'WhoId', 'WhatId', $activityFieldType];\n\n foreach ($additionalFields as $additionalField) {\n $fields[] = $additionalField->crm_provider_id;\n }\n\n $fields = array_unique($fields);\n\n // Find task with the same Outreach id as the call id.\n $query = 'SELECT ' . implode(',', $fields) . '\n FROM Task\n WHERE IsArchived = false AND IsDeleted = false';\n\n foreach ($filters as $key => $value) {\n $key = preg_quote($key, '/');\n $key = str_replace(['\\'', '\"'], '', $key);\n // Prepare the substitution.\n $strKey = \":$key\";\n\n $query .= \" AND $key $operator $strKey\";\n }\n\n $query .= ' ORDER BY LastModifiedDate DESC LIMIT 1';\n\n $objects = $this->queryHandler->query($query, $filters);\n\n // There should be only one task related to this call if any.\n if ($objects->count() === 1) {\n $object = $objects->current();\n\n $dueDate = $object['ActivityDate'] ? Carbon::parse($object['ActivityDate'])->toIso8601String() : null;\n\n $data = array_merge($object, [\n 'crmId' => $object['Id'],\n 'subject' => $object['Subject'],\n 'summary' => $object['Description'],\n 'due' => $dueDate,\n 'Type' => $object[$activityFieldType],\n ]);\n }\n } catch (NoResultsException $e) {\n // Filters don't match any records.\n } catch (ServiceUnavailableException $serviceUnavailableException) {\n // Service cannot be queried. We should probably log this.\n }\n\n return $data;\n }\n\n /**\n * Get Salesforce fields including datetime fields\n *\n * @param $objectType\n */\n private function getAllFieldsAsArray($objectType): array\n {\n $basicFields = [];\n // Not all users have access to all object fields.\n if ($this->profile->{$objectType . '_fields'}) {\n $basicFields = explode(',', $this->profile->{$objectType . '_fields'});\n }\n\n $extraFields = [\n 'CreatedDate',\n 'LastModifiedDate',\n 'IsDeleted',\n ];\n\n if ($objectType === self::OBJECT_OPPORTUNITY\n && $this->config->opportunity_value_field_id\n && ! in_array($this->config->opportunityValueField->crm_provider_id, $basicFields)\n ) {\n $extraFields[] = $this->config->opportunityValueField->crm_provider_id;\n }\n\n return array_unique(array_merge($basicFields, $extraFields));\n }\n\n /**\n * Generate transcription for activity description.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! ($this->config->store_transcript)) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n return $this->transcriptionService\n ->findTranscriptionByActivity($activity)\n ->map(static function (array $transcriptionSegment): string {\n return $transcriptionSegment['formattedStartsAt'] . ' | ' . $transcriptionSegment['transcript'];\n })\n ->implode(PHP_EOL);\n }\n\n /**\n * Find related Salesforce event based on activity data\n *\n * @return array<string>\n */\n public function fetchRelatedActivity(Activity $activity): array\n {\n $this->logger->info('[Salesforce] Searching for related activity', [\n 'activityId' => $activity->getUuid(),\n 'ownerId' => $this->profile?->crm_provider_id,\n ]);\n\n $sfEvent = $this->fetchRelatedEvent($activity);\n if (empty($sfEvent)) {\n $this->logger->info('[Salesforce] No related activity found', [\n 'activityId' => $activity->getUuid(),\n 'ownerId' => $this->profile?->crm_provider_id,\n 'account' => $activity->hasAccount()\n ? $activity->getAccount()->getCrmProviderId()\n : null,\n ]);\n\n return [];\n }\n\n return $sfEvent;\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n if ($activity->isTypeConference() === false) {\n return null;\n }\n\n if ($activity->hasActualStartTime() === false && $activity->hasScheduledStartTime() === false) {\n return null;\n }\n\n if (! $activity->hasProspect()) {\n $this->logger->info('[Salesforce] Skip look up, Activity not linked to Lead, Contact or Account', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n $playbook = $this->getPlaybook($activity->getUser());\n if ($playbook !== null && $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_TASK) {\n $this->logger->info('[Salesforce] Skip auto-sync for task-based playbook', [\n 'activityUuid' => $activity->getUuid(),\n 'playbookId' => $playbook->getId(),\n 'playbookType' => $playbook->getActivityType(),\n ]);\n\n return null;\n }\n\n try {\n $sfEvent = $this->fetchRelatedActivity($activity);\n if (empty($sfEvent)) {\n return null;\n }\n\n [$activityField, $activityType] = $this->resolveActivityTypeFromEvent($activity, $sfEvent);\n\n $this->logger->info('[Salesforce] Found related activity', [\n 'activityId' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n 'activityFieldName' => $activityField,\n 'crmActivityType' => ($activityField !== null && isset($sfEvent[$activityField]))\n ? $sfEvent[$activityField]\n : null,\n 'activityType' => $activityType,\n ]);\n\n $userId = $this->findRelatedActivityUserId($activity, $sfEvent);\n\n if ($activity->getUserId() !== $userId) {\n $this->logger->info('[Salesforce] Updating meeting owner', [\n 'activityId' => $activity->getUuid(),\n 'oldUserId' => $activity->getUserId(),\n 'newUserId' => $userId,\n ]);\n }\n\n $this->updateSfEventDescription($activity, $sfEvent);\n\n $activity->update([\n 'user_id' => $userId,\n 'crm_provider_id' => $sfEvent['Id'],\n 'playbook_category_id' => $activityType->id ?? $activity->getCategory()?->getId(),\n ]);\n\n $this->logger->info('[Salesforce] Activity updated', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return $activity;\n } catch (\\Exception $exception) {\n \\Sentry::captureException($exception);\n\n throw $exception;\n }\n }\n\n /**\n * @param array<string, mixed> $sfEvent\n *\n * @return array{0: string|null, 1: mixed}\n */\n private function resolveActivityTypeFromEvent(Activity $activity, array $sfEvent): array\n {\n $activityField = $this->getActivityFieldName($activity);\n $activityType = null;\n\n if ($activityField !== null && ! empty($sfEvent[$activityField])) {\n $playbook = $this->getPlaybook($activity->getUser());\n $activityType = $this->getPlaybookCategory($playbook, strval($sfEvent[$activityField]));\n }\n\n return [$activityField, $activityType];\n }\n\n /**\n * @param array<string> $sfEvent\n */\n private function findRelatedActivityUserId(Activity $activity, array $sfEvent): int\n {\n $userId = $activity->getUserId();\n\n if (empty($sfEvent['OwnerId']) === false) {\n $profile = $this\n ->config\n ->profiles()\n ->where('crm_provider_id', $sfEvent['OwnerId'])\n ->get()\n ->filter(static function (Profile $profile) use ($activity): bool {\n if (! $activity->isTypeConference()) {\n return ! empty($profile->user) ? $profile->user->isStatusActive() : false;\n }\n\n $participants = $activity->getParticipants();\n\n return ! empty($profile->user)\n ? $profile->user->isStatusActive()\n && $profile->user->hasPermission(PermissionEnum::RECORD_MEETING)\n && $participants->contains('user_id', $profile->user_id)\n : false;\n })\n ->first();\n\n if ($profile) {\n $userId = $profile->user_id;\n }\n }\n\n return $userId;\n }\n\n /**\n * @param array<string, mixed> $sfEvent\n */\n private function updateSfEventDescription(Activity $activity, array $sfEvent): void\n {\n try {\n if (str_contains($sfEvent['Description'], $activity->id_string)) {\n return;\n }\n\n $payload = [\n 'Description' => $sfEvent['Description']\n . PHP_EOL\n . PHP_EOL\n . new DecorateActivity()->generateDescription($activity),\n ];\n\n $this->logger->info('[Salesforce] Update record', [\n 'activityId' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n 'payload' => $payload,\n ]);\n\n $payload = array_merge(\n $payload,\n $this->payloadBuilder->fetchCustomFieldData($activity, Field::OBJECT_EVENT)\n );\n\n $this->updateRecord('Event', $sfEvent['Id'], $payload);\n } catch (\\Exception) {\n $this->logger->error('[Salesforce] Failed to update record', [\n 'activityUuid' => $activity->getUuid(),\n 'sfEvent' => $sfEvent['Id'],\n ]);\n }\n }\n\n /**\n * Returns the most recently modified Event within time range (if any).\n *\n * @return array|null An Event record from Salesforce.\n */\n private function fetchRelatedEvent(Activity $activity): ?array\n {\n $ownerId = $this->profile?->crm_provider_id;\n if ($ownerId === null) {\n return [];\n }\n\n /** @var ?Carbon $from */\n /** @var ?Carbon $to */\n [$from, $to] = $this->getFromToDates($activity);\n\n try {\n $whoId = null;\n $hasWho = $activity->lead_id || $activity->contact_id;\n if ($hasWho) {\n $whoId = $activity->hasLead()\n ? $activity->getLead()->crm_provider_id\n : $activity->getContact()->crm_provider_id;\n }\n\n if ($hasWho === false && $activity->account_id === null) {\n return null;\n }\n\n $query = $this->buildFetchRelatedEventQuery($activity);\n\n $objects = $this->queryHandler->query($query, [\n 'ownerId' => $ownerId,\n 'whoId' => $whoId,\n 'whatId' => $activity->hasOpportunity() ? $activity->getOpportunity()->crm_provider_id : null,\n 'accountId' => $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null,\n 'from' => $from?->format('Y-m-d\\TH:i:s\\Z'),\n 'to' => $to?->format('Y-m-d\\TH:i:s\\Z'),\n ]);\n\n foreach ($objects as $object) {\n return $object;\n }\n } catch (NoResultsException $e) {\n return [];\n }\n\n return [];\n }\n\n private function getFromToDates(Activity $activity): array\n {\n $from = null;\n $to = null;\n\n /** @var ?CalendarEvent $calendarEvent */\n $calendarEvent = $activity->calendarEvent()->first();\n if ($calendarEvent !== null) {\n $from = $calendarEvent->getStartTime();\n $to = $calendarEvent->getEndTime();\n }\n\n // For non-calendar imported activities\n // Also double check if calendar event dates could be null?\n // If null use what we've got so far\n if ($from === null || $to === null) {\n $from = $activity->hasScheduledStartTime()\n ? $activity->getScheduledStartTime()\n : $activity->getActualStartTime();\n $to = $activity->hasScheduledEndTime()\n ? $activity->getScheduledEndTime()->addMinutes(15)\n : $activity->getActualEndTime();\n }\n\n return [$from, $to];\n }\n\n /**\n * Determines the appropriate activity field name for querying Salesforce events.\n *\n * This method follows a hierarchy to determine the field name:\n * 1. Uses the playbook's activity field if it exists and is in the profile's accessible fields\n * 2. Falls back to the default activity field if the profile has no event fields configured\n * 3. Returns null if no suitable field is found\n *\n * @param Activity $activity The activity to determine the field for\n *\n * @return string|null The field name to use in queries, or null if none is available\n */\n private function getActivityFieldName(Activity $activity): ?string\n {\n if ($this->profile === null) {\n $this->logger->warning('[Salesforce] Cannot determine activity field - profile not found', [\n 'activityId' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n $profileEventFields = $this->profile->getFieldsAsArray('event');\n\n if (empty($profileEventFields)) {\n $defaultActivityField = $this->getDefaultActivityField(Field::OBJECT_EVENT);\n $defaultFieldName = $defaultActivityField?->getAttribute('crm_provider_id');\n // Profile not yet synced — fall back to the default activity field.\n // There is a small chance that the profile won't have Default Activity Type field access\n // in which case the query will fail.\n // This is however an edge case and should be reviewed for profile sync issues.\n Sentry::withScope(function (\\Sentry\\State\\Scope $scope) use ($defaultFieldName): void {\n $scope->setContext('details', [\n 'profileId' => $this->profile->id,\n 'defaultField' => $defaultFieldName,\n ]);\n Sentry::captureMessage(\n '[Salesforce] Profile event fields empty, falling back to default activity field.',\n \\Sentry\\Severity::warning()\n );\n });\n\n return $defaultFieldName;\n }\n\n $playbook = $this->getPlaybook($activity->getUser());\n\n if (! is_null($playbook) && ! is_null($playbook->getActivityField())) {\n $playbookFieldName = $playbook->getActivityField()->getAttribute('crm_provider_id');\n\n if (in_array($playbookFieldName, $profileEventFields, true)) {\n return $playbookFieldName;\n }\n\n $this->logger->warning('[Salesforce] Playbook activity field not found in profile fields', [\n 'activityId' => $activity->getUuid(),\n 'playbookField' => $playbookFieldName,\n 'profileId' => $this->profile->id,\n ]);\n }\n\n return null;\n }\n\n private function buildFetchRelatedEventQuery(Activity $activity): string\n {\n $hasWho = $activity->lead_id || $activity->contact_id;\n\n $activityFieldName = $this->getActivityFieldName($activity);\n $fields = array_filter(['Id', 'Description', 'OwnerId', $activityFieldName]);\n\n $ownerCondition = '(OwnerId = :ownerId OR CreatedById = :ownerId)';\n\n $query = '\n SELECT ' . implode(',', $fields) . '\n FROM Event\n WHERE ' . $ownerCondition . '\n AND IsArchived = false\n AND IsAllDayEvent = false\n AND StartDateTime >= :from\n AND EndDateTime <= :to\n AND (';\n\n $operator = '';\n if ($activity->account_id) {\n // This covers events tied to a related contact or opportunity too.\n $query .= 'AccountId = :accountId';\n\n $operator = ' OR ';\n }\n\n if ($hasWho) {\n $query .= $operator . 'WhoId = :whoId';\n\n // If we are also going to check on a specific opportunity, set that up.\n if ($activity->opportunity_id) {\n $query .= ' OR WhatId = :whatId';\n }\n }\n\n $query .= ') ORDER BY LastModifiedDate DESC';\n\n return $query;\n }\n\n public function fetchProspect(array $task): array\n {\n $lead = $account = $opportunity = $contact = $stage = $countryCode = null;\n $externalId = $task['WhoId'] ?? null;\n\n // Lead or Contact\n if ($externalId) {\n try {\n [$lead, $account, $opportunity, $contact, $stage, $countryCode] = $this->parseRecords($externalId);\n } catch (\\InvalidArgumentException $exception) {\n // Invalid object type.\n }\n }\n\n // If we happen to know the opportunity or account from the Task, figure that out.\n if (empty($task['WhatId']) === false) {\n // WhatId could be either Account ID or Opportunity ID.\n // If WhatId is Opportunity ID, get the opportunity and stage from the CRM.\n try {\n [, $account, $opportunity, , $stage, ] = $this->parseRecords($task['WhatId']);\n } catch (\\InvalidArgumentException $exception) {\n // Invalid object type.\n }\n }\n\n return [$lead, $account, $opportunity, $contact, $stage, $countryCode];\n }\n\n /**\n * Save activity transcription summary as note\n */\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n return $this->saveNote($title, $body, (string) $objectId);\n }\n\n public function getObjectByFilterConditions(string $objectType, array $fields, array $filters): ?array\n {\n if ($this->profile === null) {\n return null;\n }\n\n $queryBuilder = app(QueryBuilder::class, [\n 'profile' => $this->profile,\n ]);\n\n $query = $queryBuilder->buildObjectSearchQuery($objectType, $fields, $filters);\n\n try {\n $objects = $this->queryHandler->query($query, $filters);\n if ($objects->count() === 1) {\n return $objects->current();\n }\n } catch (\\Exception $e) {\n $this->logger->info('[Salesforce] Failed to execute query', [\n 'query' => $query,\n 'error' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n private function getCustomProfileRules(TeamRepository $teamRepository): array\n {\n $teamSettings = $teamRepository->getTeamSetting($this->team, 'custom_profile_validation');\n\n if ($teamSettings instanceof TeamSettings && $teamSettings->getValueType() === 'array') {\n $customRules = json_decode($teamSettings->getValue(), true);\n if (is_array($customRules)) {\n return $customRules;\n }\n }\n\n return [];\n }\n\n private function customProfileValidation(array $crmUser, array $customRules): bool\n {\n foreach ($customRules as $customRule) {\n if ($crmUser[$customRule['field']] !== $customRule['value']) {\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * When syncing Contact / Lead / Account / Opportunity / Stage crm entities,\n * validate and restore locally trashed objects,\n * before updating them. Objects are identified by CrmProviderId\n */\n private function restoreAnyTrashedEntity(HasMany $targetEntity, string $crmProviderId): void\n {\n $recordExists = $targetEntity->withTrashed()->where(['crm_provider_id' => $crmProviderId])->first();\n if ($recordExists && $recordExists->trashed()) {\n $recordExists->restore();\n }\n }\n\n #[\\Override] public function supportsNotes(): bool\n {\n return true;\n }\n\n private function getOwnerProfile(?string $ownerId): ?Profile\n {\n if ($ownerId === null) {\n return null;\n }\n\n return $this->config->profiles()\n ->where('crm_provider_id', $ownerId)\n ->first();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-2717453437712659029
|
-7851921920897119291
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
246
3
21
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Services\Crm\Salesforce;
use Carbon\Carbon;
use Exception;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Jiminny\Component\Country\CountriesMap;
use Jiminny\Contracts\Acl\PermissionEnum;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\FetchRelatedActivityInterface;
use Jiminny\Contracts\Services\Crm\ImportsBusinessProcessesInterface;
use Jiminny\Contracts\Services\Crm\LayoutManagementInterface;
use Jiminny\Contracts\Services\Crm\MatchCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\Provider\SalesforceBatchSyncInterface;
use Jiminny\Contracts\Services\Crm\Provider\SalesforceInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityLookupInterface;
use Jiminny\Contracts\Services\Crm\RemoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\RemoteNoteEntityManipulationInterface;
use Jiminny\Contracts\Services\Crm\SearchTaskInterface;
use Jiminny\Contracts\Services\Crm\SendSummaryToCrmInterface;
use Jiminny\Contracts\Services\Crm\SettingsInterface;
use Jiminny\Contracts\Services\Crm\SupportsObjectTypeParseInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmEntitiesInterface;
use Jiminny\Contracts\Services\Crm\SyncCrmProfileRecordTypesInterface;
use Jiminny\Contracts\Services\Crm\VerifyTaskExistsInterface;
use Jiminny\Enums\CrmObject;
use Jiminny\Events\Activities\Crm\LeadConverted;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\HttpBadRequestException;
use Jiminny\Exceptions\HttpNotFoundException;
use Jiminny\Exceptions\NoResultsException;
use Jiminny\Exceptions\ServiceUnavailableException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Jobs\Crm\MatchActivitiesToNewOpportunity;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Contact;
use Jiminny\Models\Contracts\ActivityContract;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\ContactRole;
use Jiminny\Models\Crm\Field;
use Jiminny\Models\Crm\Profile;
use Jiminny\Models\Crm\RecordType;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Playbook;
use Jiminny\Models\SocialAccount;
use Jiminny\Models\Stage;
use Jiminny\Models\TeamSettings;
use Jiminny\Models\User;
use Jiminny\Repositories\Crm\ContactRoleRepository;
use Jiminny\Repositories\Crm\FieldRepository;
use Jiminny\Repositories\Crm\ProfileRepository;
use Jiminny\Repositories\Crm\RecordTypeFieldValuesRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\Helpers\ArrayIterator;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Services\Crm\ResolveCompanyNameByEmailTrait;
use Jiminny\Services\Crm\Salesforce\Fields\FieldHelper;
use Jiminny\Services\Crm\Salesforce\Fields\FieldTypeConverter;
use Jiminny\Services\Crm\Salesforce\Fields\ValueNormalizer;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\FollowupActivityTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\LogActivityTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\RecordManipulationsTrait;
use Jiminny\Services\Crm\Salesforce\ServiceTraits\SyncFieldsTrait;
use Jiminny\Utils\CurrencyFormatter;
use Jiminny\Utils\StringUtil;
use Ramsey\Uuid\Uuid;
use Sentry\Laravel\Facade as Sentry;
class Service extends BaseService implements
SalesforceInterface,
SalesforceBatchSyncInterface,
SyncCrmEntitiesInterface,
SyncCrmProfileRecordTypesInterface,
ImportsBusinessProcessesInterface,
RemoteEntityManipulationInterface,
FetchRelatedActivityInterface,
SendSummaryToCrmInterface,
MatchDomainByEmailInterface,
SearchTaskInterface,
LayoutManagementInterface,
SettingsInterface,
MatchCrmEntitiesInterface,
RemoteEntityLookupInterface,
SupportsObjectTypeParseInterface,
RemoteNoteEntityManipulationInterface,
VerifyTaskExistsInterface
{
use ResolveCompanyNameByEmailTrait;
use SyncFieldsTrait;
use DeleteObjectsTrait;
use RecordManipulationsTrait;
use ServiceTraits\BatchSyncTrait;
use FollowupActivityTrait;
use LogActivityTrait;
/**
* Note Body Limit for the Old Note-Taking Tool
*
* @var int
*/
private const int CLASSIC_NOTE_MAX_LENGTH = 32000;
/**
* Note Content Limit for the New Notes
*
* @var int
*/
private const int ENHANCED_NOTE_MAX_LENGTH = 50000000;
private const string INSTALLED_PACKAGE_ID = '033Tw0000007bKbIAI';
private const int CACHE_TTL = 600;
private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day - 86400
/**
* @var Client
*/
protected $client;
protected PayloadBuilder $payloadBuilder;
protected QueryHandler $queryHandler;
private OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
public function __construct(
Client $client,
PayloadBuilder $payloadBuilder,
protected Dispatcher $eventDispatcher,
private readonly CountriesMap $countriesMap,
private readonly ProspectPhotoPathService $prospectPhotoPathService,
) {
parent::__construct();
$this->client = $client;
$this->payloadBuilder = $payloadBuilder;
$this->queryHandler = app(QueryHandler::class, [
'client' => $this->client,
'logger' => $this->logger,
]);
$this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [
'client' => $this->client,
]);
}
public function getDisplayName(): string
{
return 'Salesforce';
}
public function getJobDelay(): int
{
return 1;
}
protected function getOAuthAccount(User $user): ?SocialAccount
{
return $user->getSocialAccount(SocialAccount::PROVIDER_SALESFORCE);
}
public function verifyTaskExists(Activity $activity): bool
{
$crmProviderId = $activity->getCrmProviderId();
$cacheKey = "crm_task_exists:{$this->config->getId()}:$crmProviderId";
return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($activity, $crmProviderId) {
$playbook = $this->getPlaybookFromActivity($activity);
if ($playbook === null) {
$this->logger->warning('[Salesforce] Cannot verify task - no playbook found', [
'activity' => $activity->getId(),
'crm_provider_id' => $crmProviderId,
]);
return false;
}
$objectType = $playbook->getActivityType() === Playbook::ACTIVITY_TYPE_EVENT ? 'Event' : 'Task';
try {
$record = $this->getRecord($objectType, $crmProviderId, ['Id', 'IsDeleted']);
return ! empty($record) && ($record['IsDeleted'] ?? false) === false;
} catch (HttpNotFoundException|HttpBadRequestException) {
$this->logger->info('[Salesforce] Activity record not found during verification', [
'activity' => $activity->getId(),
'object_type' => $objectType,
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return false;
}
});
}
public function query(string $queryToRun, array $parameters = []): QueryIterator
{
// Due to poorly designed external calls, this method cannot be entirely removed
return $this->queryHandler->query($queryToRun, $parameters);
}
/*=========== Organization Information ===============*/
/**
* Get a list of all the API Versions for the instance.
*
* @throws CrmException
*
* @return mixed
*
*/
public function getApiVersions()
{
$url = $this->config->crm_base_url . '/services/data';
$response = $this->client->get($url);
return json_decode($response->getBody(), true);
}
/**
* Gets the valid recordTypes for a given Salesforce Object via the describe API.
*/
private function getRecordTypes(string $crmObject): array
{
$url = $this->client->getObjectsUrl() . $crmObject . '/describe';
$response = $this->client->get($url);
$jsonResponse = json_decode($response->getBody(), true);
$fields = [];
foreach ($jsonResponse['recordTypeInfos'] as $row) {
$fields[] = ['recordTypeId' => $row['recordTypeId'], 'default' => $row['defaultRecordTypeMapping']];
}
return $fields;
}
/**
* Convert raw field data into a format compatible with CRM APIs.
*/
public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string
{
return ValueNormalizer::normalize($fieldType, $fieldValue, $internal);
}
/**
* @inheritdoc
*/
public function getDefaultFields(string $activityType): array
{
$fields = [];
$defaultFields = ($activityType === Playbook::ACTIVITY_TYPE_TASK)
? FieldDefinitions::defaultTaskFields()
: FieldDefinitions::defaultEventFields();
// This lazy creates these fields if not already setup.
foreach ($defaultFields as $defaultField) {
$fields[] = $this->config->fields()->firstOrCreate($defaultField);
}
return $fields;
}
/**
* @inheritdoc
*/
public function getDefaultActivityField(string $activityType): Field
{
// Setup the activity field as the default Type.
/** @var Field $activityField */
$activityField = $this->config->fields()->where([
'crm_provider_id' => 'Type',
'object_type' => $activityType,
])->first();
return $activityField;
}
/**
* @inheritdoc
*/
public function getSupportedPlaybookTypes(): array
{
return [Playbook::ACTIVITY_TYPE_TASK, Playbook::ACTIVITY_TYPE_EVENT];
}
protected function getDefaultFollowupLayoutFields(string $activityType): array
{
$fields = [];
$fieldRepo = app(FieldRepository::class);
$fieldFilter = ($activityType === Playbook::ACTIVITY_TYPE_TASK)
? FieldDefinitions::taskFollowupFieldsFilter()
: FieldDefinitions::eventFollowupFieldsFilter();
foreach ($fieldFilter as $eachFilter) {
$field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $eachFilter);
// Only add the field if it is created, which it should be.
if ($field) {
$fields[] = $field;
}
}
return $fields;
}
public function getDealInsightsFields(): array
{
return FieldDefinitions::dealInsightsFields();
}
/**
* This one is now called only when ImportActivityTypes is triggered or SyncFieldMetadata executed manually
* Regular sync now uses SharedSyncFieldsTrait -> syncSingleObjectType
* Needs to be replaced later on
*/
public function syncField(Field $field): void
{
try {
if (FieldHelper::isCustomField($field)) {
$query = '
SELECT
Id, Metadata, TableEnumOrId
FROM
CustomField
WHERE
DeveloperName = :fieldName
AND
TableEnumOrId = :fieldType
AND
NamespacePrefix = :namespacePrefix';
// We need to constrain the field lookup to the object, in case it's used in multiple places.
$objectType = \in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true)
? 'activity'
: $field->object_type;
$sfFields = $this->queryHandler->metadata($query, [
'fieldName' => substr($field->crm_provider_id, 0, -\strlen('__c')),
'fieldType' => ucfirst($objectType),
// This is used to ensure we only consider the field within the org, not installed packages.
'namespacePrefix' => 'null',
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
// Sync field metadata.
$metadata = $sfField['Metadata'];
$field->description = mb_strimwidth($metadata['description'] ?? '', 0, 191);
$field->label = mb_strimwidth($metadata['label'] ?? '', 0, Field::LABEL_MAX_LENGTH);
$field->type = $this->convertFieldType($metadata['type'], $field->getEntityName());
$field->is_mandatory = ($metadata['required'] === true);
$field->length = $metadata['length'];
$field->default_value = mb_strimwidth(trim($metadata['defaultValue'] ?? '', '"'), 0, 191);
$field->save();
} else {
$query = '
SELECT
Id, DataType, DeveloperName, Label, Length, Description
FROM
FieldDefinition
WHERE
DurableId = :entityName';
$entityName = $field->getEntityName();
$sfFields = $this->queryHandler->metadata($query, [
'entityName' => $entityName,
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
$convertedType = $this->convertFieldType($sfField['DataType'], $entityName);
$label = mb_strimwidth($sfField['Label'], 0, Field::LABEL_MAX_LENGTH);
if ($field->isBusinessType()) {
$label = 'Opportunity Type';
}
$field->description = mb_strimwidth($sfField['Description'], 0, Field::DESCRIPTION_MAX_LENGTH);
$field->label = $label;
$field->type = $convertedType;
$field->length = $sfField['Length'];
$field->save();
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
private function convertFieldType(string $from, ?string $entityName = null): string
{
$converter = new FieldTypeConverter();
return $converter->convert($from, $entityName);
}
/**
* @inheritdoc
*/
public function importPicklistValues(Field $field): array
{
$values = [];
$fieldValues = [];
try {
if (FieldHelper::isCustomField($field)) {
$query = '
SELECT
Id, Metadata, TableEnumOrId
FROM
CustomField
WHERE
DeveloperName = :fieldName
AND
TableEnumOrId = :fieldType
AND
NamespacePrefix = :namespacePrefix';
// We need to constrain the field lookup to the object, in case it's used in multiple places.
$objectType = \in_array($field->object_type, [Field::OBJECT_TASK, Field::OBJECT_EVENT], true) ?
'activity' : $field->object_type;
$sfFields = $this->queryHandler->metadata($query, [
'fieldName' => substr($field->crm_provider_id, 0, -\strlen('__c')),
'fieldType' => ucfirst($objectType),
// This is used to ensure we only consider the field within the org, not installed packages.
'namespacePrefix' => 'null',
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
$valueSet = $sfField['Metadata']['valueSet'];
if ($valueSet['valueSetName'] === null) {
// Local picklist values can be obtained easily.
$picklistValues = $valueSet['valueSetDefinition']['value'];
} else {
// But for some fields, we just get the Global Value Picklist pointer so need to do more work.
$picklistValues = $this->importGlobalValuePicklistValues($valueSet['valueSetName']);
}
// Import all active values.
foreach ($picklistValues as $i => $sfFieldValue) {
// Setup default value.
if ($sfFieldValue['default']) {
$field->update(['default_value' => $sfFieldValue['valueName']]);
}
// This comes through as null if active (lol).
if ($sfFieldValue['isActive'] !== false) {
$values[] = [
'value' => $sfFieldValue['valueName'],
'label' => $sfFieldValue['valueName'],
'sequence' => $i,
'is_default' => $sfFieldValue['default'],
];
}
}
} else {
$objectFields = $this->getObjectFields($field->object_type);
$fieldId = $field->crm_provider_id;
// Only work with our field of interest.
$objectField = array_filter($objectFields, function ($item) use ($fieldId) {
return $item['name'] === $fieldId;
});
$objectField = array_shift($objectField);
if (empty($objectField['picklistValues']) === false) {
foreach ($objectField['picklistValues'] as $i => $sfFieldValue) {
// Skip inactive values.
if ($sfFieldValue['active'] === false) {
continue;
}
// Setup default value.
if ($sfFieldValue['defaultValue']) {
$field->update(['default_value' => $sfFieldValue['value']]);
}
$values[] = [
'value' => $sfFieldValue['value'],
'label' => $sfFieldValue['label'],
'sequence' => $i,
'is_default' => $sfFieldValue['defaultValue'],
];
}
}
}
$fieldsToPurge = $field->values()->get()->pluck('value')->toArray();
foreach ($values as $value) {
$value['value'] = substr($value['value'] ?? '', 0, 255);
$fieldValues[] = $field->values()->updateOrCreate([
'value' => $value['value'],
], $value);
// Remove this value from the ones we are going to purge.
if (($key = array_search($value['value'], $fieldsToPurge, true)) !== false) {
unset($fieldsToPurge[$key]);
}
}
// Delete the old values that are no longer used.
// Get IDs of the values to be deleted
$valuesToDelete = $field->values()->whereIn('value', $fieldsToPurge);
$valuesToDeleteIds = $valuesToDelete->pluck('id');
if (! $valuesToDeleteIds->isEmpty()) {
$recordTypeFieldValuesRepository = app(RecordTypeFieldValuesRepository::class);
$recordTypeFieldValuesRepository->deleteForCrmFieldValueIds($valuesToDeleteIds->toArray());
// Now safely delete from crm_field_values
$valuesToDelete->delete();
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
return $fieldValues;
}
/**
* Gets values from Global Value Picklists.
*/
private function importGlobalValuePicklistValues(string $picklistName): array
{
$query = '
SELECT
Metadata
FROM
GlobalValueSet
WHERE
DeveloperName = :picklistName
LIMIT 1';
try {
$sfValues = $this->queryHandler->metadata($query, [
'picklistName' => $picklistName,
]);
// There is always 1 result at this point.
$sfValue = $sfValues->current();
return $sfValue['Metadata']['customValue'];
} catch (NoResultsException $noResultsException) {
// Nothing returned.
return [];
}
}
/**
* @inheritdoc
*/
public function syncProfileRecordTypes(): void
{
$objectTypes = [
'lead',
'account',
'contact',
'opportunity',
'task',
'event',
];
foreach ($objectTypes as $objectType) {
try {
$crmRecordTypes = $this->getRecordTypes(ucfirst($objectType));
foreach ($crmRecordTypes as $crmRecordType) {
// If the record type is default and not the Master type, set this.
if ($crmRecordType['default'] && $crmRecordType['recordTypeId'] !== '012000000000000AAA') {
$recordType = $this->config->recordTypes()
->where('crm_provider_id', $crmRecordType['recordTypeId'])
->first();
if ($recordType) {
$this->profile->{$objectType . '_record_type_id'} = $recordType->id;
}
}
}
} catch (HttpNotFoundException $exception) {
Log::error('No access to ' . $objectType . ' object, skipping...');
// XXX: should we log this fact somewhere?
continue;
}
}
if ($this->profile->isDirty()) {
$this->profile->save();
}
}
/**
* Gets business processes.
*/
public function importBusinessProcesses(): void
{
$query = '
SELECT
Id, IsActive, Name, TableEnumOrId
FROM
BusinessProcess
WHERE
TableEnumOrId IN (\'Lead\',\'Opportunity\')';
try {
$sfProcesses = $this->queryHandler->query($query);
// Upsert all processes for the team.
foreach ($sfProcesses as $sfProcess) {
/** @var BusinessProcess $businessProcess */
$businessProcess = $this->config->businessProcesses()->updateOrCreate([
'crm_provider_id' => $sfProcess['Id'],
], [
'team_id' => $this->team->id,
'name' => $sfProcess['Name'],
'type' => $sfProcess['TableEnumOrId'] === 'Lead' ? 'lead' : 'opportunity',
'is_selectable' => $sfProcess['IsActive'],
]);
$this->importBusinessProcessStages($businessProcess);
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* Gets business process stages.
*/
private function importBusinessProcessStages(BusinessProcess $businessProcess): void
{
$query = '
SELECT
Metadata
FROM
BusinessProcess
WHERE
Id = :processId';
try {
$stages = [];
$sfProcessStages = $this->queryHandler->metadata($query, [
'processId' => $businessProcess->crm_provider_id,
]);
// There is always 1 result at this point.
$sfProcessStage = $sfProcessStages->current();
// Upsert all processes for the team.
foreach ($sfProcessStage['Metadata']['values'] as $sfProcessStage) {
$sanitizedName = urldecode($sfProcessStage['valueName']); // Must decode: "%2C" becomes "," etc.
$stage = $businessProcess->crm->stages()
// This MUST match on label because this API doesn't use API Name.
->where('label', $sanitizedName)
->where('type', $businessProcess->type)
->where('is_selectable', 1)
->first();
if ($stage) {
$stages[] = $stage->id;
}
}
$businessProcess->stages()->sync($stages);
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* Gets record types.
*/
public function importRecordTypes(): void
{
$query = '
SELECT
Id, IsActive, Name, BusinessProcessId, SobjectType
FROM
RecordType';
try {
$sfRecordTypes = $this->queryHandler->query($query);
// Upsert all record types for the process.
foreach ($sfRecordTypes as $sfRecordType) {
$businessProcess = null;
if ($sfRecordType['BusinessProcessId']) {
$businessProcess = $this->config->businessProcesses()
->where('crm_provider_id', $sfRecordType['BusinessProcessId'])
->first();
}
/** @var RecordType $recordType */
$recordType = $this->config->recordTypes()->updateOrCreate([
'crm_provider_id' => $sfRecordType['Id'],
], [
'team_id' => $this->team->id,
'type' => mb_strtolower($sfRecordType['SobjectType']),
'name' => $sfRecordType['Name'],
'is_selectable' => $sfRecordType['IsActive'],
'business_process_id' => $businessProcess->id ?? null,
]);
$this->importRecordTypeFieldValues($recordType);
}
} catch (NoResultsException $noResultsException) {
// Do nothing.
}
}
/**
* Import record type - field value mappings. This only works for standard fields.
*/
private function importRecordTypeFieldValues(RecordType $recordType): void
{
try {
$query = '
SELECT
Metadata
FROM
RecordType
WHERE
Id = :recordTypeId';
$sfFields = $this->queryHandler->metadata($query, [
'recordTypeId' => $recordType->crm_provider_id,
]);
// There is always 1 result at this point.
$sfField = $sfFields->current();
// Sync field metadata.
$picklists = $sfField['Metadata']['picklistValues'];
foreach ($picklists as $picklist) {
$field = $this->config->fields()->where([
'type' => Field::TYPE_PICKLIST,
'object_type' => $recordType->type,
'crm_provider_id' => $picklist['picklist'],
])->first();
if ($field) {
$fieldValues = [];
foreach ($picklist['values'] as $value) {
// Must decode: "%2C" becomes "," etc.
$fieldValue = $field->values()
->where('value', urldecode($value['valueName']))
->first();
if ($fieldValue) {
$fieldValues[] = $fieldValue->id;
}
}
$recordType->fieldValues()->sync($fieldValues);
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
}
/**
* @inheritdoc
*/
public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage
{
$params = [];
$missingStage = null;
if ($types === null) {
$types = [Stage::TYPE_LEAD, Stage::TYPE_OPPORTUNITY];
}
foreach ($types as $type) {
if ($type === Stage::TYPE_LEAD) {
$query = '
SELECT
Id, ApiName, MasterLabel, SortOrder
FROM
LeadStatus';
} else {
$query = '
SELECT
Id, ApiName, MasterLabel, IsActive, SortOrder, DefaultProbability
FROM
OpportunityStage';
}
if ($missingStageName) {
$escapedStageName = ValueNormalizer::replaceQueryWithStringLiterals($missingStageName);
$query .= ' WHERE ApiName = :stageName';
$params = [
'stageName' => $escapedStageName,
];
}
try {
$sfStages = $this->queryHandler->query($query, $params);
} catch (NoResultsException $exception) {
$sfStages = [];
}
$missingStage = null;
// Upsert all stages for the team.
foreach ($sfStages as $sfStage) {
$selectable = true;
if (array_key_exists('IsActive', $sfStage)) {
$selectable = $sfStage['IsActive'];
}
$this->restoreAnyTrashedEntity($this->config->stages(), $sfStage['Id']);
$stage = $this->config->stages()->updateOrCreate([
'crm_provider_id' => $sfStage['Id'],
], [
'team_id' => $this->team->id,
'name' => mb_strimwidth($sfStage['ApiName'], 0, 50),
'label' => mb_strimwidth($sfStage['MasterLabel'], 0, 191),
'type' => $type,
'sequence' => $sfStage['SortOrder'] ?? 0,
'is_selectable' => $selectable,
'probability' => $sfStage['DefaultProbability'] ?? null,
]);
if ($missingStageName && $missingStageName === $sfStage['ApiName']) {
$missingStage = $stage;
}
}
if ($missingStageName && $missingStage === null) {
// If they requested a stage that still doesn't exist, it must be inactive so lazy create it.
$missingStage = $this->config->stages()->create([
'crm_provider_id' => Uuid::uuid4(),
'team_id' => $this->team->id,
'name' => mb_strimwidth($missingStageName, 0, 50),
'label' => mb_strimwidth($missingStageName, 0, 191),
'type' => $type,
'sequence' => 0,
'is_selectable' => 0,
]);
}
}
return $missingStage;
}
/**
* @inheritdoc
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
$syncCount = 0;
$fields = $this->getAllFieldsAsArray('lead');
if (\in_array('Id', $fields, true) === false) {
return $syncCount;
}
$query = '
SELECT ' . rtrim(implode(',', $fields), ',') . '
FROM Lead
WHERE LastModifiedDate > :since
ORDER BY LastModifiedDate ASC';
try {
$sfLeads = $this->queryHandler->query($query, [
'since' => $since->format('Y-m-d\TH:i:s\Z'),
]);
foreach ($sfLeads as $sfLead) {
// Only sync if previously imported.
if ($this->hasLead($sfLead['Id'])) {
$this->importLead($sfLead);
$syncCount++;
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::LEAD);
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
$fields = $this->getAllFieldsAsArray('lead');
$sfLead = $this->getRecord('Lead', $crmId, $fields);
return $this->importLead($sfLead);
}
private function importLead($crmData): ?Lead
{
/** @var ?Stage $stage */
$stage = null;
if (isset($crmData['Status'])) {
// Get the current stage.
$stage = $this->config
->stages()
->where('name', $crmData['Status'])
->where('type', Stage::TYPE_LEAD)
->first();
if ($stage === null) {
// Import it.
$stage = $this->importStages([Stage::TYPE_LEAD], $crmData['Status']);
}
}
// If we have no way of importing this, just return null :(
if ($stage === null) {
return null;
}
$countryCode = $crmData['CountryCode'] ?? null;
// Salesforce allows custom "countries" to be created. Disregard these.
if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {
$countryCode = null;
}
// If we have no country code, try to parse it from the country name.
if ($countryCode === null && empty($crmData['Country']) !== false) {
$countryCode = $this->convertCountryNameToCode($crmData['Country']);
}
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['Phone'] ?? '', 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
$mobilePhone = null;
if (empty($crmData['MobilePhone']) === false) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['MobilePhone'], 0, 25);
$mobilePhone = phone_e164($countryCode, $number);
}
$convertedDate = null;
$convertedAccount = null;
$convertedOpportunity = null;
$convertedContact = null;
if ($crmData['IsConverted'] == 'true') {
$convertedDate = $crmData['ConvertedDate'];
if (empty($crmData['ConvertedAccountId']) === false) {
$convertedAccount = $this->config
->accounts()
->where('crm_provider_id', $crmData['ConvertedAccountId'])
->first();
if ($convertedAccount === null) {
try {
$convertedAccount = $this->syncAccount($crmData['ConvertedAccountId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
if (empty($crmData['ConvertedOpportunityId']) === false) {
$convertedOpportunity = $this->config
->opportunities()
->where('crm_provider_id', $crmData['ConvertedOpportunityId'])
->first();
if ($convertedOpportunity === null) {
try {
$convertedOpportunity = $this->syncOpportunity($crmData['ConvertedOpportunityId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
if (empty($crmData['ConvertedContactId']) === false) {
$convertedContact = $this->team
->crm
->contacts()
->where('crm_provider_id', $crmData['ConvertedContactId'])
->first();
if ($convertedContact === null) {
try {
$convertedContact = $this->syncContact($crmData['ConvertedContactId']);
} catch (HttpNotFoundException $exception) {
// Probably the user has no permissions to access the converted data.
}
}
}
}
if (empty($crmData['Company'])) {
$company = 'Unknown';
} else {
$company = mb_strimwidth($crmData['Company'], 0, 191);
}
$domain = null;
if (empty($crmData['Website']) === false) {
$domain = mb_strimwidth($crmData['Website'], 0, 191);
$domain = StringUtil::resolveDomain($domain);
}
$createdDate = null;
if (empty($crmData['CreatedDate']) === false) {
$createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');
}
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$data = [
'team_id' => $this->team->id,
'user_id' => $profile?->user_id,
'owner_id' => $crmData['OwnerId'] ?? '',
'company' => $company,
'domain' => $domain,
'name' => $crmData['Name'] ? mb_strimwidth($crmData['Name'], 0, 191) : '',
'title' => $crmData['Title'] ? mb_strimwidth($crmData['Title'], 0, 128) : null,
'email' => $crmData['Email'] ? mb_strimwidth($crmData['Email'], 0, 80) : null,
'phone' => $parsedNumber['phone'],
'ext' => $parsedNumber['ext'] ?? null,
'mobile_phone' => $mobilePhone,
'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(
crmConfiguration: $this->config,
crmProviderId: $crmData['Id'],
modelType: Lead::class,
fileName: $crmData['Id'],
avatarText: $crmData['Name']
),
'stage_id' => $stage->id,
'record_type_id' => null,
'converted_at' => $convertedDate,
'converted_account_id' => $convertedAccount->id ?? null,
'converted_opportunity_id' => $convertedOpportunity->id ?? null,
'converted_contact_id' => $convertedContact->id ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $createdDate,
];
$this->restoreAnyTrashedEntity($this->config->leads(), $crmData['Id']);
/** @var Lead $lead */
$lead = $this->config->leads()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);
if ($lead->wasChanged('converted_at') && $lead->getConvertedAt() !== null) {
$this->eventDispatcher->dispatch(new LeadConverted($lead));
}
$this->handleObjectDeletion($lead, $crmData);
if ($lead->trashed()) {
return null;
}
return $lead;
}
/**
* @inheritdoc
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
$fields = $this->getAllFieldsAsArray('account');
if (\in_array('Id', $fields, true) === false) {
return $syncCount;
}
$query = '
SELECT ' . rtrim(implode(',', $fields), ',') . '
FROM Account
WHERE LastModifiedDate > :since
ORDER BY LastModifiedDate ASC';
try {
$sfAccounts = $this->queryHandler->query($query, [
'since' => $since->format('Y-m-d\TH:i:s\Z'),
]);
foreach ($sfAccounts as $sfAccount) {
// Only sync if previously imported.
if ($this->hasAccount($sfAccount['Id'])) {
$this->importAccount($sfAccount);
$syncCount++;
}
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::ACCOUNT);
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
$fields = $this->getAllFieldsAsArray('account');
if (! in_array('Id', $fields, true)) {
$this->logger->info('[Salesforce] Sync account cancelled. Fields are not available.', [
'crmId' => $crmId,
'userId' => $this->profile->getUserId(),
]);
return null;
}
$sfAccount = $this->getRecord('Account', $crmId, $fields);
return $this->importAccount($sfAccount);
}
private function importAccount($crmData): ?Account
{
if (! empty($crmData['IsDeleted'])) {
$this->handleEntityDeletionByProviderId($this->config->accounts(), $crmData);
return null;
}
$countryCode = $crmData['BillingCountryCode'] ?? $crmData['ShippingCountryCode'] ?? null;
// Salesforce allows custom "countries" to be created. Disregard these.
if ($countryCode && $this->countriesMap->countryExists($countryCode) === false) {
$countryCode = null;
}
// If we have no country code, try to parse it from the country names.
if ($countryCode === null && empty($crmData['BillingCountry']) === false) {
$countryCode = $this->convertCountryNameToCode($crmData['BillingCountry']);
}
if ($countryCode === null && empty($crmData['ShippingCountry']) === false) {
$countryCode = $this->convertCountryNameToCode($crmData['ShippingCountry']);
}
if (empty($crmData['Phone']) === false) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($crmData['Phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$industry = null;
if (empty($crmData['Industry']) === false) {
$industry = mb_strimwidth($crmData['Industry'], 0, 40);
}
$domain = null;
if (empty($crmData['Website']) === false) {
$domain = mb_strimwidth($crmData['Website'], 0, 191);
$domain = StringUtil::resolveDomain($domain);
}
$createdDate = null;
if (empty($crmData['CreatedDate']) === false) {
$createdDate = Carbon::parse($crmData['CreatedDate'])->setTimezone('UTC');
}
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$data = [
'team_id' => $this->team->id,
'user_id' => $profile?->user_id,
'owner_id' => $crmData['OwnerId'],
'name' => mb_strimwidth($crmData['Name'], 0, 191),
'photo_path' => $this->prospectPhotoPathService->getOrGeneratePhotoPath(
crmConfiguration: $this->config,
crmProviderId: $crmData['Id'],
modelType: Account::class,
fileName: $crmData['Id'],
avatarText: $crmData['Name']
),
'industry' => $industry,
'domain' => $domain,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $createdDate,
];
$this->restoreAnyTrashedEntity($this->config->accounts(), $crmData['Id']);
/** @var Account $account */
$account = $this->config->accounts()->updateOrCreate(['crm_provider_id' => $crmData['Id']], $data);
$this->handleObjectDeletion($account, $crmData);
return $account->trashed() ? null : $account;
}
/**
* @inheritdoc
*/
public function syncOpportunities(array $parameters, ?string $strategy = null): int
{
$strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);
$syncCount = 0;
$logParams = $parameters;
$parameters['profile'] = $this->profile;
$logParams['user'] = $this->profile->getUserId();
if (count($strategies) > 1) {
$this->logger->warning('[' . $this->getDisplayName() . '] Multiple sync strategies used', [
'teamId' => $this->team->getUuid(),
'params' => $logParams,
'strategies_count' => count($strategies),
]);
}
foreach ($strategies as $syncStrategy) {
$name = $syncStrategy->getStrategyName();
try {
$sfOpportunities = $syncStrategy->fetchOpportunities($parameters);
$totalRecords = $sfOpportunities->count();
foreach ($sfOpportunities as $sfOpportunity) {
$this->importOpportunity($sfOpportunity);
$syncCount++;
}
} catch (NoResultsException $noResultsException) {
// Nothing to sync.
$this->logger->warning('[' . $this->getDisplayName() . '] No opportunities found', [
'teamId' => $this->team->getUuid(),
'name' => $name,
'params' => $logParams,
'reason' => $noResultsException->getMessage(),
]);
} catch (CrmException $crmException) {
// Nothing to sync.
$this->logger->warning('[' . $this->getDisplayName() . '] Opportunity sync failed', [
'teamId' => $this->team->getUuid(),
'name' => $name,
'params' => $logParams,
'reason' => $crmException->getMessage(),
]);
}
}
$this->syncRemotelyDeletedObjectsWithErrorHandling(CrmObject::OPPORTUNITY, ['params' => $logParams]);
// debug to see how if count of opportunities reaches 1000
if ($syncCount >= 1000) {
$this->logger->info(
'[' . $this->getDisplayName() . '] Sync Opportunities - count warning',
[
'team_id' => $this->team->getId(),
'params' => $logParams,
'count' => $syncCount,
'strategies_count' => count($strategies),
'total_records' => $totalRecords ?? null,
]
);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncOpportunity(string $crmId): ?Opportunity
{
$strategy = $this->opportunitySyncStrategyResolver->resolve(
$this->config,
OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY
);
$parameters = [
'profile' => $this->profile,
'crm_id' => $crmId,
];
try {
$sfOpportunity = $strategy->fetchOpportunities($parameters);
} catch (HttpNotFoundException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [
'teamId' => $this->team->id_string,
'crmId' => $crmId,
]);
return null;
} catch (CrmException $crmException) {
$this->logger->info('[' . $this->getDisplayName() . '] Opportunity sync failed', [
'teamId' => $this->team->id_string,
'crmId' => $crmId,
'exception' => $crmException->getMessage(),
]);
return null;
}
if ($sfOpportunity instanceof ArrayIterator) {
return $this->importOpportunity($sfOpportunity->getItems());
}
return $this->importOpportunity($sfOpportunity);
}
/**
* @throws HttpNotFoundException
*/
private function importOpportunity($crmData): ?Opportunity
{
$profile = $this->getOwnerProfile($crmData['OwnerId'] ?? null);
$account = null;
if (empty($crmData['AccountId']) === false) {
/** @var ?Account $account */
$account = $this->config->accounts()
->where('crm_provider_id', (string) $crmData['AccountId'])
->first();
if ($account === null) {
$account = $this->syncAccount($crmData['AccountId']);
}
}
$userId = $profile?->getUserId() ?? $account?->getUserId();
if ($userId === null) {
$this->logger->error('[Salesforce] | Skip import, no user_id found', [
'id' => $crmData['Id'],
]);
return null;
}
/** @var ?Stage $stage */
$stage = null;
if (i...
|
85698
|
NULL
|
NULL
|
NULL
|
|
85698
|
2937
|
8
|
2026-05-28T12:57:48.043104+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973068043_m2.jpg...
|
PhpStorm
|
faVsco.js – Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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.8374335,"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":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"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 'TextRelayServiceTest'","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 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-5491484594888514086
|
-7050964252790406720
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
rapstomViewNeweNNCCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-lproidet•) Service.php x php heipers.phg© Querylterator.phpу queуkesui onoservice one©) SyncBatchRedisService.ofd Traitsooaseeenone© BaseService.phpoCachecimsenroo© CountryCodeResoiver.prpC) CrmActivityProviderintegrateC CrmActivitvService.ohoccrmcont curationsettinos.ser© CrmObiectsResoiver.phpC) DeraultProsoec SearchStrateEmailHelper.phpFindsProspectinterface.php© LayoutManager.php© MatchDomainByEmailnterfac©OpportunityActivityMatcher.genoorur wsvoesttatkouote@ OnnorturitvSvoeStrtaosBaeC [EMAIL](c ProspectSearchStrategyFact® ProspectSearchStrategyinter(© ProviderRegistry.phpd PocordColoator nhoResolveCompanyNameByEm:© TimePerioditerator.php@ impori@b internalUsciko cooscrytrionyTeom.oneo Nemeuoninanespace Jaminny servaces urn salestorce› use .8B8832258Ca Maiii>D Actions>D Office> E Repositories.>M Resoivers>E Traite>M Validators.© BatchCriteria.ohe©BatchCriterisInterface.oho©.BatchService.php8 BatchServiceinterace ono©[EMAIL] MnChsond Conhto nhn111119class service excends Baseservice Inplemencswles torceinertideSalesforceBatchSynCInterface,Sunc tontheeuis interthceSunctontrorer arecord woes ncerthcImportsBusinessProcesses.nterfaceRemoteEntityManipulationInterfaceFetchRelatedActivity.nterface,SendSummaryToCrminterfaceMatchDomainByEmailInterface,SearchTaskInterfaceLayoutManagementInterfaceSettingsInterface.Matchinintr des ntertaceSupports0b:lectTypeParseInterfaceRemoteNoteEntityManipulationInterfaceVonCutaclsysotcintonfanduse ResolveCompanyNaneByEnailTraituse syncrelostraseuse Delete0biectsTraituse kecororanzoulaczonstrazcuse ServiceTraits\BatchSyncTraituse FoLlowupac.v.cylraze* Note Body Liait for the Old Note-Toking Too*Avar snt1 usageonivate const int GLASSTC NOTE MAX LENGTH = 32600.*Note Pontent Hmit fon the New Notes*Avan snt1 usagdprivate const int ENHANCED NOTE MAX LENGTH = 58000800:©ActivityControlier.php© Client.phpa| 5A SF fiminny@localhost)console (PROD) x C Service.php# console fiaumA console [STAGING)D0000Ty: Autow714715-718719720BEERRBREBERRERTYINTERVAL 38 DAY.BY u.id, u.email, u.name, u.softphone numbenBY sns_count DESC;045 A1 A41 У 66 A* fron teans where id = 1;NCAT(u.id, CASE WHEN u.id = t.ouner_id THEN • (ouner)* ELSE ** END) AS user_idouner_id FROM social_accounts saeans t 1.nc->1ontd n.teandU.tean_id = 1117 and sa.provider = "hubspot":* FROM activities WHERE uuid_to_bin(*8824fffb-2df7-4817-91f4-d9f896050248') = uuid; # 79933459 YES* FROM activities WHERE Uuid_to_bin('[CREDIT_CARD]-927f-444da2a8185c') = uuid; # 80186192 NO*EROM eon confauratzons WHERE 3d = 1953%* FROM teans WHERE 1d = 1117;* Eoon ucene whena 3d = 30240.* fron playbooks where id = 5473;* fron playbook categonies where id = 43783:* fron playbook categonies where playbook_id = 5473:*Eoon enn flalde whons id = 45022.* fron cra_field values where cra_field_id = 659242:* FROM crn_field_data fdcrnfields f ON fd.crm field id = f.ioactivities a ON fd.activity_id = a.idactivity_id = 79933459f.crn_provider_id = 'hs activity_type'* FROM activity-messages:* fron text. relayswhere created at > *2926-05-91*-* fron actsivities where user_id TN (7168, 18688) and created at > 2926-85-22' orden by id desciteam.1d = 1 and id TN (18688, 13934. 7168)=*£ron activities where usen 1a = 7169 order bu ja desc 1ini+ 18:*fnon accounts where tean 3a = 1and nane = "Colunos*.*Toon tasne whendae*foon Antéusty seanches whene usen $a = 71852.*Eoon sctkusty coanch Eilitane nhans actkusty cancch fa TM CRRRR2 RROQ2=AGe naetAn7EKTO0У L7Thu 28 May 15:57:47lestorce Pnonth+0 .Tocbzancone noosle tiveic toneueDAAtPrOne"IKiSoaredmpryl SparsortsPmbei/euil so u Str-Code, SaroNmoe e(), 10, °) 5,*):private function parseContactMobile(?string ScountryCode, array ScrmData): 2stringLet me first look at the PR diff to understand the changescommand co..g• cd /Users/lukas/minny/app od git dift origin/master...HEAD -nane-on Lyspp/nicco/conerotters/ara acaivatyconcrotter-pnccts/lntt/Scrysces/crm/Saleetorce/Scrvicetcet.ohCommand col.ose/Servi/es/er/Satesyore/Servite-php origin/saster.. HEAD -aanica/ ra/a a orce/ervice pe asrie teo erentssthis-shandle0biectDeletion(Slead, ScraData):te 1s/uns//5krv/ces/crv/Splestorte/Servcrtest-Paster..HEAD -atruncete a ape taeontectefelation mmethod sherera nell Resu tie tune1t0;ScontactsRelatsion-seethod ("updateOrGreate*)-swill|Return(Scontact)scontip = ethodscontaetsc)-x1t1gururn(scortacs/Relation):Ask anything (XOL)c HodeAdhotv• OXtModtur Tatme Toxcsehireht4 spad...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85697
|
2936
|
4
|
2026-05-28T12:57:45.962129+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973065962_m1.jpg...
|
PhpStorm
|
faVsco.js – Service.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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":"TextRelayServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4899909956188268022
|
-8629512240427235104
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
iTerm2• • 0ShellEditViewSessionScripts|ProfilesWindowHelp-zshDOCKERO ₴1DEV (docker)₴82-zshN3screenpipe"O ₴4-zshapp/Component/ES/Repositories/EsResetActivityRepository.phpapp/Component/KeyPoints/Services/KeyPointsIndexingService.php348+++app/Component/TranscriptionSummary/Services/GetTranscriptionSummaryService.phpapp/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpapp/Models/Activity/Moment.phpapp/Models/Activity/Note.php402116app/Models/CommentAbstract.php++++app/Models/Crm/FieldData.phpapp/Models/ElasticSearch/ActivityElasticSearchTrait.php651+++++++=app/Models/Participant.phpapp/Traits/RequiresUUID.php5scripts/run_command_stagetests/Unit/Component/ActionItems/Services/ActionItemsIndexingServiceTest.phptests/Unit/Component/KeyPoints/Services/GetKeyPointsServiceTest.phptests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phptests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.php71346976++++++++++17 files changed, 472 insertions(+), 22 deletions(-)create mode 100644 app/Component/ActionItems/Services/ActionItemsIndexingService.phpcreate mode 100644 app/Component/KeyPoints/Services/KeyPointsIndexingService.phpcreate mode 100644 app/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingService.phpcreate mode 100644 tests/Unit/Component/ActionItems/Services/ActionItemsIndexingServicelest.phpcreate mode 100644 tests/Unit/Component/KeyPoints/Services/KeyPointsIndexingServiceTest.phpcreate mode 100644 tests/Unit/Component/TranscriptionSummary/Services/TranscriptionSummaryIndexingServiceTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ ;xddocker exec-it docker_lamp_1 bash -c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity) $ ;xddockerexec -itdocker_lamp_1 bash-c "mv /usr/local/etc/php/conf.d/xdebug.ini ~/xdebug.ini"mv: cannot stat '/usr/local/etc/php/conf.d/xdebug.ini': No such file or directoryWhat's next:Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug docker_lamp_1Learn more at [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20963-fix-import-on-deleted-entity)$D< →0 lahl85ec2-user@ip-10-30-129-...100% <78• Thu 28 May 15:57:451₴1ec2-user@ip-10-30-140-...₴7...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
85696
|
2937
|
7
|
2026-05-28T12:57:44.889049+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779973064889_m2.jpg...
|
PhpStorm
|
faVsco.js – Service.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest...
|
[{"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":"#12121 on JY-20963-fix-import-on-deleted-entity, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11569149,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12121 exists for current branch JY-20963-fix-import-on-deleted-entity","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.8374335,"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":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-5714658179048184897
|
-8634297297851446848
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12121 on JY-20963-fix-im Project: faVsco.js, menu
#12121 on JY-20963-fix-import-on-deleted-entity, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
rapstomViewNeweNNCCoocWindowFV faVsco.|s ~#12121 on JY-20963-fx-lProinet v•) Service.php x php heipers.phg© Querylterator.phpу queуkesui onoservice one©) SyncBatchRedisService.ptd Traitsooaseeenone© BaseService.phpoCachecimsenroo© CountryCodeResoiver.prpC) CrmActivityProviderintegrateC CrmActivitvService.ohocermeont ourationsettinos.ver© CrmObiectsResoiver.phpC) DeraultProsoec SearchStrateEmailHelper.php© FindsProspectinterface.php© LayoutManager.php8 MatchDomainByEmailinterfac©OpportunityActivityMatcher.genoorur wsvoesttatkouote@ OnnorturitvSvoeStrtaosBaeC [EMAIL](c ProspectSearchStrategyFact® ProspectSearchStrategyinter(© ProviderRegistry.phpd PocordColoator nhoResolveCompanyNameByEm:© TimePerioditerator.php@ impori@b internalUsciko cooscrytrionyTeom.oneo Nemeuoninanespace Jaminny servaces urn salestorce› use .8B8832258Ca Maiii>D Actions>D Office> E Repositories.>M Resoivers>E Traite>M Validators.©) BatchCriteria.ohd©BatchCriterisInterface.oho©.BatchService.php8 BatchServiceinterace ono©[EMAIL] MnChsond Conhto nhn111119class service excends Baseservice Inplemencswles torceinertideSalesforceBatchSynCInterface,SunctontmerelignterthchSunctnntror aredond Moeg ncertacImportsBusinessProcesses.nterfaceRemoteEntityManipulationInterfaceFetchRelatedActivity.nterface,SendSummaryToCrminterfaceMatchDomainByEmailInterface,SearchTaskInterfaceLayoutManagementInterfaceSettingsInterface.Matchinintr des ntertaceSupports0b:lectTypeParseInterfaceRemoteNoteEntityManipulationInterfaceVonCutaclsysotcintonfanduse ResolveCompanyNaneByEnailTraituse syncrelostraseuse Delete0biectsTraituse kecororanzoulaczonstrazcuse ServiceTraits\BatchSyncTraituse FoLlowupac.v.cylraze* Note Body Liait for the Old Note-Toking Too*Avar snt1 usageonivate const int GLASSTC NOTE MAX LENGTH = 32600,*Note Pontent Himit fon the New Notes*Avan snt1 usagdprivate const int ENHANCED NOTE MAX LENGTH = 58000800:©ActivityControlier.phpA SF fiminny@localhost)console (PROD) x C Service.php# console fiaum© Client.phpa| 5A console [STAGING)D0.000as aua a an - aTy: Autow714715-718719720BEERRBREBERRERTYINTERVAL 38 DAY.BY u.id, u.email, u.name, u.softphone numbenBY sns_count DESC;045 A1 A41 У 66 A* fron teans where id = 1;NCAT(u.id, CASE WHEN u.id = t.ouner_id THEN • (ouner)* ELSE ** END) AS user_idouner_id FROM social_accounts saeans t 1.nc->1ontd n.teandU.tean_id = 1117 and sa.provider = "hubspot":* FROM activities WHERE uuid_to_bin(*8824fffb-2df7-4817-91f4-d9f896050248') = uuid; # 79933459 YES* FROM activities WHERE Uuid_to_bin('[CREDIT_CARD]-927f-4f4da2a8185c') = uuid; # 80186192 NO*EROM eon confauratzons WHERE 3d = 1953%* FROM teans WHERE 1d = 1117;* Eoon ucene whena 3d = 30240.* fron playbooks where id = 5473;* fron playbook categonies where id = 43783:* fron playbook categonies where playbook_id = 5473:*foon enn ffalde whond id = 450222.* fron cra_field values where cra_field_id = 659242:* FROM crn_field_data fdcrnfields f ON fd.crm field id = f.ioactivities a ON fd.activity_id = a.idactivity_id = 79933459f.crn_provider_id = 'hs activity_type'* FROM activity-messages:* fron text. relayswhere created at > *2826-05-91*•* fron actsivities where user_id TN (7168, 18688) and created at > 2926-85-22' orden by id desciteam.1d = 1 and id TN (18688, 13934. 7168)=*£ron activities where usen 1a = 7169 order bu ja desc 1ini+ 18:*fnon accounts where tean 3a = 1and nane = "Colunos,.*Toon tasne whendae*foon Antáusty seanches whane usen $a = 71852.*Eoon sctkusty coanch Eilitane nhans actkusty cancch fa TM CRRRR2 RROQ2TO0У L7Thu 28 May 15:57:44oles orce Phont+0 .es. Sti there is something missing private functionMDAANChOnA'Ine1,25, ")):private function parseContactMobile(?string ScountryCode, array ScrmData): ?stringLet me first look at the PR diff to understand the changes• cd /Users/lukas/jiminny/app &d git diff origin/master...HEAD --nanc-onLy)tests/lnt/Scrysces/crm/Sallestorce/Seryicetest.ohce /Services/cr/Satesyore/Servite php origin/saster...HEAD -cepService-phgsthis-shandle0biectDeletion(Slead, ScrmData):command co..gte 1s/uns//5krv/ces/crv/Splestorte/Servcrtest-Paster..HEAD -=AGe naetAn7EKAsk anything (XoL)Adhotv• OXtModtur Tatme Toxcsehireht4 spad...
|
85695
|
NULL
|
NULL
|
NULL
|