|
58720
|
2089
|
16
|
2026-05-19T13:31:01.921490+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779197461921_m2.jpg...
|
PhpStorm
|
faVsco.js – WebhookController.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormINavicarecodeFV faVsco.js°9 JY-20915-add-d PhostormINavicarecodeFV faVsco.js°9 JY-20915-add-domainProiect vc) MailboxController.pnp©messagecontroller.onpg weonookcontroller.onp› D Voice) Audiocontroller.pnpc basecontroller.onov D Webhook• D Hubspot›D IntearationAppsubscriptionsC) ActivityProviderController.phoC) ActivitvTranscriptioncontroller.phoc) BaseController.ohpP CalendarController.ohnc) Revortcontrolller.onvP SoftohoneWebhookController.ohpC AbstractController.ohoCommentContextinterface.php© ConferencesOptinOutController.php•) Controller nhn© ExportController.php© FrontendController.phpT FrontendControllerTrait.php8 GeocodingController.php©healncheckcontroller.ono92 liveCoachController.phpMissing leamcontroller.ongMobllecontroller.onocNotricationcontroller.ono© NotificationProviderController.phpPlaybackController.phpPlaylistController.phpc) Pushercontroller.phpSlackController.phpSupportController.phpc)Teamsetuocontroller.ono© UserAutomatedReportsController.phnC) WelcomeController.ohoMiddlewareRequests/ ResourcesM ResnonsasM SerializersM Transformers@ KernelnhnT PlaylistTrackResourceTrait.phpT. ValidateCrmConnectionRequiredTrait nhn.• M IntearationsM Intoractionsi lohe• MActivitvM AiAntomation19.01.18 Graham21.01.18 Granam15.07.19 Petkansk19.01.18 Graham3819.01.1819.01.18 Graham22.01.18 Graham9.05.102A 01.18 Grahan19.01.18 Granan19.01.18 Grahan19.01.18 Granan19.01.18 Grahan7.10.19Shit1.10.191.10.1919.01.18 Graham12.02.18 Graham18.112219.[IP_ADDRESS] Graham12.02.18 Graham12.02.18 Graham18.11.229.05.18Graham005 [IP_ADDRESS] 1919.01.18 Graham19.01.18 Graham11.10.24 Papazo11.10.24 Papazo2510.21 KulaiZo.lu.Ll KuloV20.0/ 26 Vacilou30.04.26 VasilevXalalwwscillow30.04.26 Vasilev30.04.26 Vasilev19.01.1% Granam19.01.1% Granam19.01.18 Graham22.03.18 Grahan22.03.18 Grahan22.03.18 Grahan9.05.1826.0118 Grahan26.0118 Graham26.0118 Grahanaster' has been restored // Rollback // Configure... (46 minutes ago)© WebhookController.php= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console (PROD] X# console [euyclass WebhookController extends BaseControllerpublic function receive(TextMessagingService SmessagingService)A17D1AA console [STAGING]$from = Sthis->request->input( key: 'From'):$to = $this->request->input( key: 'To'):Smessage = Sthis->request->input( key: 'Body'):SprospectId = Sthis->request->cookie( key: 'prospectId');sobpecclype = schis->request->cooklel Key. ob ecclype)Smessageld = Sthis->request->input( key: 'MessageSid')573// CNAM dataScountryCode = Sthis->request->input( key: 'FromCountry') ?: null:suser = Lache:: remembersto.tti: 3600, function " use (Sto) {Sto)->firstOrFail0:581- 582if we don't know who it 1s from. check the cache to see 1f we've seen thaScacheKey = 'user:' . Suser->id_string •':prospect#' . $from;1+ Sorospectid za= null xxcache::nos(Scachekev)<585586587Sprospectid = Cache::get($cacheKey):Scachekey = 'user.t, Suser->id strina ":obiect-tvoe#t Sfrom:if (SobjectType === null && \Cache::has(ScacheKey)) 1SobjectType =\Cache::get($cacheKey):589-590591SmessagingService->setTeam(Suser->team):593T594595Sactivity = Observer::skipEvents(function () use ($messagingService, Smessageld, Suser, $from, $to, $messaI/ We have deferred ElasticSearch updates. Let's update it noweventnew Updatesinqueent1tyaentityId: Sactivity->qetIdo.purpose: 'telephony-webhook-controller-receive'597- [PHONE]01602603604605606eventnew Recervedsactivitv)):-- 608609// If possible. respond with a cookie to add state. favouring the opportunity.ifSactivity->oooortunitv) "Cookie: : queue( ...oaram'prospectid', Sactivity->ooportunitv->crm orov:/1z•elseif ((Sactivitv->orospect) ^s: 'prospectid', Sactivity->prospect->crm_providet615neturn resnonsed->twimloCONCAT(U.Id, CASE WHEN U.Id = t.owner_Zd THEN ' (owner)" ELSE''END) AS user_1d -41 41 440 V.65 Asa.*t.owner_id FROM social_accounts saouin usens u on u.id e sasociable 1dOIN teams t .n<->l: on t.id = u.team_idHERE u.team_id = 581 and sa.provider = 'salesforce';ELECT * FROM automated_report_results order by id desc;elect * from features;elect * from team_features where feature_id = 40;elect * from teams where id = 556;elect * trom aucomated reports"here id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044,["pdf" "podcast"]ELECT * FROM automated report results WHERE uuid to bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid:elect * from automated_report_results order by id desc;ELECT * FROM automated report resultsWHEkE 10 = 1714"elect * from automated report results WHERE report id = 54:elect * From opportunities where 1d = 75945491ELE * FROM teams WHERE name LIKE "%lesx: # 711. 692. 16067 -Timinnvintearationdlesmills.comelect * from playbooks where team id = 711: # event 226147ELEc * FROM Dlavbook catedories WHERE DLavbook 1d = 55151ELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event':ELECT * SROM eom fields WHEREI1d = 2261471ELECT * FROM crm_field_values WHERE crm_field_id = 226147;ELECT * FROM crm_configurations WHERE id = 692)SLSCTCONCAT(u.id, CASE WHEN u.id = t.owner_id THEN '(ownen)I FISE II EMO) AS ucen idnemailsa.*,t.owner id FROM social accounts saOIN users u on u.id = sa.sociable ioOIN teams t 1..n<->1: on t.id = u.team idHERE u.team id = 711 and sa.provider = 'salesforce':ELECT * FROM crm_profiles co JOIN users u 1..n<->1: on u.id = co.user_id WHERE u.team_id = 711:elect * from leadselect * from calendarsELECTt1d AS team1d100% 5• Tue 19 May 16:31:0'AskJiminnyReportActivityServiceTest vCascadeFix AutomatedReportsCascadeText Relay Email Routt+O •where is this "To send a message back., lust reoiv to this email. in the whole tlow? don't understand the wholeInougntror is?l'll search for the reoiv messade text and trace the comolete text relav flow to answer vour questions.searched lo send a message back in ~/iminny/appSearched reolv to this emaillin ~/iminnvlaools-received.blade.oho and EmailTextRelav.ohoThouaht for 1sThought for 3sSearched class Received in ~/iminnvlaooInoughttor ssInougnt tor zsThought for 2sText Relay Flow Explanation1. "To send a message back. just reoiv to this email " Location-received.blade. php (line 37)2. How Text Relay WorksPhase 1: Inbound SMS Received (Prospect → Jiminny User)• Prospect sends SMS to Jiminny user's softohone numberTwilio wehhook hits wehhookController:•receive/)l• Activity created with type TYPE SMS INBOUND• Received event fired (line 881• CreateReceivedNot ification listener sends email to userPhase 2. Emaill Notitication• SmsReceived notification sends email usina sms-received. blade. oho temolate• Email includes the message content and "To send a message back, just reply to this email."Critical: The email has a special replvto addressAsk anvthina (884-L)« Code SWF-1.6Accent alliWN Windsurf Teams82-20UTE.8Po 4 spaces...
|
NULL
|
-1758904069026336316
|
NULL
|
click
|
ocr
|
NULL
|
PhostormINavicarecodeFV faVsco.js°9 JY-20915-add-d PhostormINavicarecodeFV faVsco.js°9 JY-20915-add-domainProiect vc) MailboxController.pnp©messagecontroller.onpg weonookcontroller.onp› D Voice) Audiocontroller.pnpc basecontroller.onov D Webhook• D Hubspot›D IntearationAppsubscriptionsC) ActivityProviderController.phoC) ActivitvTranscriptioncontroller.phoc) BaseController.ohpP CalendarController.ohnc) Revortcontrolller.onvP SoftohoneWebhookController.ohpC AbstractController.ohoCommentContextinterface.php© ConferencesOptinOutController.php•) Controller nhn© ExportController.php© FrontendController.phpT FrontendControllerTrait.php8 GeocodingController.php©healncheckcontroller.ono92 liveCoachController.phpMissing leamcontroller.ongMobllecontroller.onocNotricationcontroller.ono© NotificationProviderController.phpPlaybackController.phpPlaylistController.phpc) Pushercontroller.phpSlackController.phpSupportController.phpc)Teamsetuocontroller.ono© UserAutomatedReportsController.phnC) WelcomeController.ohoMiddlewareRequests/ ResourcesM ResnonsasM SerializersM Transformers@ KernelnhnT PlaylistTrackResourceTrait.phpT. ValidateCrmConnectionRequiredTrait nhn.• M IntearationsM Intoractionsi lohe• MActivitvM AiAntomation19.01.18 Graham21.01.18 Granam15.07.19 Petkansk19.01.18 Graham3819.01.1819.01.18 Graham22.01.18 Graham9.05.102A 01.18 Grahan19.01.18 Granan19.01.18 Grahan19.01.18 Granan19.01.18 Grahan7.10.19Shit1.10.191.10.1919.01.18 Graham12.02.18 Graham18.112219.[IP_ADDRESS] Graham12.02.18 Graham12.02.18 Graham18.11.229.05.18Graham005 [IP_ADDRESS] 1919.01.18 Graham19.01.18 Graham11.10.24 Papazo11.10.24 Papazo2510.21 KulaiZo.lu.Ll KuloV20.0/ 26 Vacilou30.04.26 VasilevXalalwwscillow30.04.26 Vasilev30.04.26 Vasilev19.01.1% Granam19.01.1% Granam19.01.18 Graham22.03.18 Grahan22.03.18 Grahan22.03.18 Grahan9.05.1826.0118 Grahan26.0118 Graham26.0118 Grahanaster' has been restored // Rollback // Configure... (46 minutes ago)© WebhookController.php= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]& console (PROD] X# console [euyclass WebhookController extends BaseControllerpublic function receive(TextMessagingService SmessagingService)A17D1AA console [STAGING]$from = Sthis->request->input( key: 'From'):$to = $this->request->input( key: 'To'):Smessage = Sthis->request->input( key: 'Body'):SprospectId = Sthis->request->cookie( key: 'prospectId');sobpecclype = schis->request->cooklel Key. ob ecclype)Smessageld = Sthis->request->input( key: 'MessageSid')573// CNAM dataScountryCode = Sthis->request->input( key: 'FromCountry') ?: null:suser = Lache:: remembersto.tti: 3600, function " use (Sto) {Sto)->firstOrFail0:581- 582if we don't know who it 1s from. check the cache to see 1f we've seen thaScacheKey = 'user:' . Suser->id_string •':prospect#' . $from;1+ Sorospectid za= null xxcache::nos(Scachekev)<585586587Sprospectid = Cache::get($cacheKey):Scachekey = 'user.t, Suser->id strina ":obiect-tvoe#t Sfrom:if (SobjectType === null && \Cache::has(ScacheKey)) 1SobjectType =\Cache::get($cacheKey):589-590591SmessagingService->setTeam(Suser->team):593T594595Sactivity = Observer::skipEvents(function () use ($messagingService, Smessageld, Suser, $from, $to, $messaI/ We have deferred ElasticSearch updates. Let's update it noweventnew Updatesinqueent1tyaentityId: Sactivity->qetIdo.purpose: 'telephony-webhook-controller-receive'597- [PHONE]01602603604605606eventnew Recervedsactivitv)):-- 608609// If possible. respond with a cookie to add state. favouring the opportunity.ifSactivity->oooortunitv) "Cookie: : queue( ...oaram'prospectid', Sactivity->ooportunitv->crm orov:/1z•elseif ((Sactivitv->orospect) ^s: 'prospectid', Sactivity->prospect->crm_providet615neturn resnonsed->twimloCONCAT(U.Id, CASE WHEN U.Id = t.owner_Zd THEN ' (owner)" ELSE''END) AS user_1d -41 41 440 V.65 Asa.*t.owner_id FROM social_accounts saouin usens u on u.id e sasociable 1dOIN teams t .n<->l: on t.id = u.team_idHERE u.team_id = 581 and sa.provider = 'salesforce';ELECT * FROM automated_report_results order by id desc;elect * from features;elect * from team_features where feature_id = 40;elect * from teams where id = 556;elect * trom aucomated reports"here id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044,["pdf" "podcast"]ELECT * FROM automated report results WHERE uuid to bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid:elect * from automated_report_results order by id desc;ELECT * FROM automated report resultsWHEkE 10 = 1714"elect * from automated report results WHERE report id = 54:elect * From opportunities where 1d = 75945491ELE * FROM teams WHERE name LIKE "%lesx: # 711. 692. 16067 -Timinnvintearationdlesmills.comelect * from playbooks where team id = 711: # event 226147ELEc * FROM Dlavbook catedories WHERE DLavbook 1d = 55151ELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event':ELECT * SROM eom fields WHEREI1d = 2261471ELECT * FROM crm_field_values WHERE crm_field_id = 226147;ELECT * FROM crm_configurations WHERE id = 692)SLSCTCONCAT(u.id, CASE WHEN u.id = t.owner_id THEN '(ownen)I FISE II EMO) AS ucen idnemailsa.*,t.owner id FROM social accounts saOIN users u on u.id = sa.sociable ioOIN teams t 1..n<->1: on t.id = u.team idHERE u.team id = 711 and sa.provider = 'salesforce':ELECT * FROM crm_profiles co JOIN users u 1..n<->1: on u.id = co.user_id WHERE u.team_id = 711:elect * from leadselect * from calendarsELECTt1d AS team1d100% 5• Tue 19 May 16:31:0'AskJiminnyReportActivityServiceTest vCascadeFix AutomatedReportsCascadeText Relay Email Routt+O •where is this "To send a message back., lust reoiv to this email. in the whole tlow? don't understand the wholeInougntror is?l'll search for the reoiv messade text and trace the comolete text relav flow to answer vour questions.searched lo send a message back in ~/iminny/appSearched reolv to this emaillin ~/iminnvlaools-received.blade.oho and EmailTextRelav.ohoThouaht for 1sThought for 3sSearched class Received in ~/iminnvlaooInoughttor ssInougnt tor zsThought for 2sText Relay Flow Explanation1. "To send a message back. just reoiv to this email " Location-received.blade. php (line 37)2. How Text Relay WorksPhase 1: Inbound SMS Received (Prospect → Jiminny User)• Prospect sends SMS to Jiminny user's softohone numberTwilio wehhook hits wehhookController:•receive/)l• Activity created with type TYPE SMS INBOUND• Received event fired (line 881• CreateReceivedNot ification listener sends email to userPhase 2. Emaill Notitication• SmsReceived notification sends email usina sms-received. blade. oho temolate• Email includes the message content and "To send a message back, just reply to this email."Critical: The email has a special replvto addressAsk anvthina (884-L)« Code SWF-1.6Accent alliWN Windsurf Teams82-20UTE.8Po 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58721
|
2089
|
17
|
2026-05-19T13:31:06.971885+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779197466971_m2.jpg...
|
PhpStorm
|
faVsco.js – WebhookController.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20915-add-domain-speci Project: faVsco.js, menu
JY-20915-add-domain-specific-email-text-relay, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20915-add-domain-specific-email-text-relay, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11269947,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20915-add-domain-specific-email-text-relay","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-6840606315183554619
|
-8780881095690048570
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20915-add-domain-speci Project: faVsco.js, menu
JY-20915-add-domain-specific-email-text-relay, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
PhostormINavicarecodeFV faVsco.js°9 JY-20915-add-domain-specifiProiect(e) MailboxController.onoclass WebhookController extends BaseControllerA17D1A©messagecontroller.onppUDLIc TUnctionrecervelexthessagzngservice smessagingservicesg weonookcontroller.onp› D Voice) Audiocontroller.pnpc basecontroller.onov D WebhookHubspot>_ IntegrationAppsubscriptionsC) ActivityProviderController.phoC) ActivitvTranscriptioncontroller.phoc) BaseController.ohpP CalendarController.ohnc) Revortcontrolller.onv@ SoftohoneWebhookController.ohvC AbstractController.oho© CommentContextinterface.php© ConferencesOptinOutController.php•) Controller nhn© ExportController.php© FrontendController.phpT FrontendControllerTrait.php8 Geocodina Controller nhn©healncheckcontroller.ono92 liveCoachController.phpMissing leamcontroller.ong(e) MobileController.pnocNotricationcontroller.ono© NotificationProviderController.phpPlaybackController.php© PlaylistController.phpc) Pushercontroller.phpSlackController.php@ SupportController.phpc)Teamsetuocontroller.ono© UserAutomatedReportsController.phnC) WelcomeController.ohoM MiddlewareRequests/ ResourcesM ResnonsasM SerializersM Transformers@ Kernelnhn$user =\Cache:: remember(Sto,ttl: 3600, function ( use ($to) {recurn user..wherel col$to)->first0rFailO:F);// If we don't know who it is from, check the cache to see if we've seen that number mapped bef 575scachekey ="user.'suser-1o scrino.prospecth . stromif (SprospectId === null && \Cache::has(ScacheKey)) {Sprospectid=Cache::qetScacheKey):ScacheKey ='user:' . Suser->1d string.obnect-type#' . Sfrom:1+ (sobnectivoe za= null c Lache:.nos(scacheKev))*- 581- 582sobnectivoe =Lache:.oetScachekev)584SmessagingService->setTeam(Suser->team)Sactivitv = Observer: •skanEventsdfunction () use (SmessagingService, $messageId, $user, $from, $to, $message, $prospectId, $ 589586587588L590)591// We have deferred ElasticSearch updates. Let's update it nowevent(new UpdateSingleEntityCentitvld• Cactivitv->nettdoiupdacelarger. upoatelargeccnum..Aclevelr,purpose: 'telephony-webhook-controller-receive',)):eventnew ReceivedSactiv1ty)*/ If possible, respond with a cookie to add state, favouring the opportunity over the prospect 601if (Sactivity->opportunity) {eters: 'prospectId'. Sactivity->opportunity->crm_provider id. 4 * 60):[PHONE]=596597- [PHONE]02603cookle:: queue ..para} elseif (Sactivitv->prospect) {cookle: queued'prospectid', Sactivity->prospect->crm_provider id, 4 * 60):@ Activitvreturn response@->twimlobEnnocnort: Procnec+tntenfacolnul1lSource: .lapp/Models/Activitv.ohoT PlaylistTrackResourceTrait.phpT. ValidateCrmConnectionRequiredTrait nhn.nnivate function fetchActivitvo• Activitv-...> M intearationspOST /wehhooklsmslevent fiinm Intoractions1A5I19>public function eventOf...h- 615)vM lohe• MActivitvDOST /wohbooklemelnatificationM AiAntomationpublic function notification: void{...hWorkcnace accociated with hranch 'macter' hac heon rectored Il Pollback II Confiaure (46 minutec aao)100% 5• Tue 19 May 16:31:06AskJiminnyReportActivityServiceTest v= laravel.log4 SF jiminny@localhost]4 HS_local [jiminny@localhost]« console [PROD] X# console [euyCascadeA console [STAGING]CONCAT(U.Id, CASE WHEN U.Id = t.owner_Zd THEN ' (owner)" ELSE''END) AS user_1d -u.emarFix AutomatedReportsCascadeText Relay Email Routi+0 •41 41 440 V.65 Asa.*t.owner_id FROM social_accounts saouin usens u on u.ide sa.sociable 1diOIN teams t 1..n<->1: on t.id = u.team_idHERE u.team_id = 581 and sa.provider = 'salesforce';ELECT * FROM automated_report_results order by id desc;elect * from features;elect * from team_features where feature_id = 40;elect * from teams where id = 556;elect * from automated reports:here id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044,["pdf" "podcast"]ELECT * FROM automated report results WHERE uuid to bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid:elect * from automated_report_results order by id desc;ELECT * FROM automated report resultsWHERE id = 1919:elect * from automated report results WHERE report id = 54:elect * from opportunities where id = 7594349:ELECT * FROM teams WHERE name LIKE '%Les%': # 711. 692. [EMAIL] * from playbooks where team id = 711: # event 226147ELECT * FROM olavbook cateaories WHERE olavbook id = 5515:ELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event':ELECT * FROM com fields WHERE1d = 2261471ELECT * FROM com field values WHERE com field id = 226147:ELECT * FROM crm_configurations WHERE id = 692)SLSCTCONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '• END) AS user_id,nemailsa.*,t.owner id FROM social accounts saOIN users u on u.id = sa.sociable ioOIN teams t 1..n<->1: on t.id = u.team idHERE u.team id = 711 and sa.provider = 'salesforce':ELECT * FROM crm_profiles co J0IN users u 1..n<->1: on u.id = cp.user id WHERE u.team_id = 711:elect * from leadselect * from calendarscicortid AS team idwhere is this "To send a message back., lust reoiv to this email. in the whole tlow? don't understand the wholeInougntror is?l'll search for the reoiv messade text and trace the comolete text relav flow to answer vour questions.searched lo send a message back in ~/iminny/appSearched reolv to this emaillin ~/iminnvlaools-received.blade.oho and EmailTextRelav.ohoThouaht for 1sThought for 3sSearched class Received in ~/iminnvlaooInoughttor ssInougnt tor zsThought for 2sText Relay Flow Explanation1. "To send a message back. just reoiv to this email " Location)-received.blade. php (line 37)2. How Text Relay WorksPhase 1: Inbound SMS Received (Prospect → Jiminny User)• Prospect sends SMS to Jiminny user's softohone numberTwilio wehhook hits wehhookController:•receive/)l• Activity created with type TYPE SMS INBOUND• Received event fired (line 881• CreateReceivedNot ification listener sends email to userPhase 2. Emaill Notitication• SmsReceived notification sends email usina sms-received. blade. oho temolate• Email includes the message content and "To send a message back, just reply to this email."Critical: The email has a special replvto addressAccent alliAsk anvthing 884-D)« Code SWF-1.6WN Windsurf Teams82-20UTE.8io 4 spaces...
|
58720
|
NULL
|
NULL
|
NULL
|
|
58722
|
2088
|
17
|
2026-05-19T13:31:22.012890+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779197482012_m1.jpg...
|
PhpStorm
|
faVsco.js – WebhookController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20915-add-domain-speci Project: faVsco.js, menu
JY-20915-add-domain-specific-email-text-relay, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20915-add-domain-specific-email-text-relay, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20915-add-domain-specific-email-text-relay","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-6840606315183554619
|
-8780881095690048570
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20915-add-domain-speci Project: faVsco.js, menu
JY-20915-add-domain-specific-email-text-relay, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1DEV (docker)₴82APP (-zsl(use "git restore ‹file›..."to discard changesin working directory)modified:env.localmodified:app/Console/Commands/JiminnyDebugCommand.phpmodified:config/logging.phpUntracked files:Cuse "git add ‹file›..."to include in what will be committed).env.nikilocal.env.otherWEBHOOK_FILTERING_IMPLEMENTATION.mdapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.phpapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.phpids.txtpublic/favicon.icoraw_sql_query.saltests/Unit/Policies/CanAccessAiReportsTest.phpchanges added to commit (use "gitadd"and/or "git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pullremote: Enumerating objects: 63, done.remote: Counting objects: 100% (63/63), done.remote: Compressing objects: 100% (17/17),remote: Total 63 (delta 46), reused 63 (delta 46), pack-reused 0 (from 0)Unpacking objects: 100% (63/63), 6.51 KiB | 77.00 KiB/s, done.From github.com:jiminny/app16fa95720c..045ab2e1748a03ca9e03..84e7b10765JY-20842-remove-elastic-search-delete-documentsJY-20943-fix-calendar-eventsUpdating 5604af40cf..177ea29b8dorigin/JY-20origin/JY-20app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonroutes/api.phptests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phptests/Unit/Component/Transcription/Service/StorageServiceTest.php10 files changed, 82 insertions(+), 172 deletions(-)delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.phpdelete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20915-add-doma*Switched to a new branch 'JY-20915-add-domain-specific-email-text-relay'lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-emai• •ED→HomeDMsActivityFilesLater•*•MoreJiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of_jimi...• Direct messagesRio Nikolay Yankov. Galya Dimitrova Me. Vasil Vasilev®. Aneliya Angelovaa. Stefka Stoyanovao Stoyan TomovBs Todor Stamatov *Mario Georgiev. Nikolay Ivanovdo James Graham8 Stoyan TanevE Lukas Kovalik y... 0::: Appsi Jira CloudToast100% <Tue 19 May 16:31:21Describe what you are looking for# releases8 226 0• MessagesC Files• Bookmarks+177ea29b- Mequest #12102 fromTodayjiminny/JY-209~e un ur ai-call-scoring-accessjiminny/app | Added by GitHubCircleCI APP 3:00 PMDeployment Successful!Project: appWhen:05/19/202612:00:02Tag:View JobNewCircleCl APP3:35 PMNew commits deployed to Prophet Prod-US:(b1b41a2)(https://github.com/jiminny/prophet/commit/b1b41a207d8271f8f3128b3633b9b70597151ea2) -[JY-20919](https://jiminny.atlassian.net/browse/JY-20919): Lower exec reports batch size #518)(steliyan-g)New commits deployed to Prophet Prod-EU:[b1b41a2)(https://github.com/jiminny/prophet/commit/b1b41a207d8271f8f3128b3633b9b70597151ea2) -JY-20919](https://jiminny.atlassian.net/browse/JY-20919): Lower exec reports batch size (#518)(steliyan-g)Message #releases+Aa......
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58723
|
2089
|
18
|
2026-05-19T13:31:22.033139+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779197482033_m2.jpg...
|
PhpStorm
|
faVsco.js – WebhookController.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20915-add-domain-speci Project: faVsco.js, menu
JY-20915-add-domain-specific-email-text-relay, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20915-add-domain-specific-email-text-relay, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11269947,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20915-add-domain-specific-email-text-relay","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8529103343812781828
|
-8708827900202079290
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20915-add-domain-speci Project: faVsco.js, menu
JY-20915-add-domain-specific-email-text-relay, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
PhostormVIewINavicarecodemelpFV faVsco.js°9 JY-20915-add-domain-specific-email-text-reroledey(e) MailboxController.ono©messagecontroller.onpg weonookcontroller.onp› D Voice) Audiocontroller.pnpc basecontroller.onov @ WebhookHubspot›D IntearationAppsubscriptionsC) ActivityProviderController.phoC) ActivitvTranscriptioncontroller.phoc) BaseController.ohpP CalendarController.ohoc) Revortcontrolller.onvP SoftohoneWebhookController.ohpC AbstractController.oho© CommentContextinterface.php© ConferencesOptinOutController.php© Controller.php© ExportController.php© FrontendController.phpT FrontendControllerTrait.php8 GeocodingController.phpc) HealthCheckcontroller.ong92 liveCoachController.phpMissing leamcontroller.ong(e) MobileController.pnocNotricationcontroller.ono© NotificationProviderController.phpPlaybackController.php© PlaylistController.phpc) Pushercontroller.phpSlackController.php@ SupportController.phpc)Teamsetuocontroller.ono© UserAutomatedReportsController.phpC) WelcomeController.ohoMiddlewareRequests/ ResourcesM ResnonsasIM SerializersM Transformers(C) Kernel.nhnlT PlaylistTrackResourceTrait.php6062T. ValidateCrmConnectionRequiredTrait nhn.> M intearations© WebhookController.php x=.env.production-euclass WebhookController extends baseconcroutenA17 81 ApUDLIc TUnctionrecervelexthessagzngservice snessagingservice.ttl: 3600, function ( use ($to) {sorcphone_number, sto,-rerscurra1loris trom, check the cache to see it we ve seen chat number mappea berore.>10 Scrino , "prospecch'.StronhCache: : has(ScacheKey)) {etScacheKey):>1d Strina.:0bnect-type#' . Sfrom:•cache: .nas(scacheKev)) *otscacheKev)vents(› gingService, $messageId, $user, $from, $to, $message, $prospectId, $objectType, $countryCode) {...}Search updates. Let's update it now+tdogetEnum: :ACTIVITYnooк-controLler-recerve,h a cookie to add state, favouring the opportunity over the prospect.ers: 'prospectId'. Sactivity->opportunity->crm_provider id. 4 * 60):ct)€ers:orospectid'.sactivity->orosoect->crm oroviden 1d. 4 * 60)*D• 2Activitv....}M intoractionc1A5I19>vM lohe• MActivitvM AiAntomationvoidt...}Workcnace accociated with hranch 'macter' hac heon rectored Il Pollback II Confiaure (46 minutec aao)100% 5• Tue 19 May 16:31:2'AskJiminnyReportActivityServiceTest v= laravel.log« SF jiminny@localhost]4 HS_local [jiminny@localhost]& console (PROD] X# console [euyCascadeA console [STAGING]CONCAT(U.Id, CASE WHEN U.Id = t.owner_Zd THEN ' (owner)" ELSE''END) AS user_1d -u.emar572575576577578579580-581- 582583584585586587588589[PHONE]93)7594595=596597- [PHONE]01602603614615)617Fix AutomatedReportsCascadeText Relay Email Routt+O •41 41 440 V.65 Asa.*t.owner_id FROM social_accounts saouin usens u on u.ide sa.sociable 1diOIN teams t 1..n<->1: on t.id = u.team_idHERE u.team_id = 581 and sa.provider = 'salesforce';ELECT * FROM automated_report_results order by id desc;elect * from features;elect * from team_features where feature_id = 40;elect * from teams where id = 556;elect * from automated reports:here id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044,["pdf" "podcast"]ELECT * FROM automated report results WHERE uuid to bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid:elect * from automated_report_results order by 1d desc;ELECT * FROM automated report resultsWHERE id = 1919:elect * from automated report results WHERE report id = 54:elect * from opportunities where id = 7594349:ELECT * FROM teams WHERE name LTKE 1%Les%': # 711. 692. 16067-jiminnvintegration0lesmills.comelect * from playbooks where team id = 711: # event 226147ELECT * FROM olavbook cateaories WHERE olavbook id = 5515:ELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';ELECT * FROM com fields WHERE1d = 2261471ELECT * FROM crm_field_values WHERE crm_field_id = 226147;ELECT * FROM crm_configurations WHERE id = 692)SLSCTCONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,nemailsa.*,t.owner id FROM social accounts saOIN users u on u.id = sa.sociable ioOIN teams t 1..n<->1: on t.id = u.team idHERE u.team id = 711 and sa.provider = 'salesforce' :ELECT * FROM crm_profiles co JOIN users u 1..n<->1: on u.id = co.user_id WHERE u.team_id = 711:elect * from leadselect * from calendarsELECTtid AS team idwhere is this "To send a message back., lust reoiv to this email. in the whole tlow? don't understand the wholeInougntror is?l'll search for the reoiv messade text and trace the comolete text relav flow to answer vour questions.searched lo send a message back in ~/iminny/appSearched reolv to this emaillin ~/iminnvlaools-received.blade.oho and EmailTextRelav.ohoThouaht for 1sThought for 3sSearched class Received in ~/iminnvlaoolInoughttor ssInougnt tor zsThought for 2sText Relay Flow Explanation1. "To send a message back. just reolv to this email " Location)-received.blade. php (line 37)2. How Text Relay WorksPhase 1: Inbound SMS Received (Prospect → Jiminny User)• Prospect sends SMS to Jiminny user's softohone numberTwilio wehhook hits wehhookController:•receive/)l• Activity created with type TYPE SMS INBOUND• Received event fired (line 881• CreateReceivedNot ification listener sends email to userPhase 2. Emaill Notitication• SmsReceived notification sends email usina sms-received. blade. oho temolate• Email includes the message content and "To send a message back, just reply to this email.'Critical: The email has a special replvto addressAccent alliAsk anvthina (884-L)« Code SWF-1.6WN Windsurf Teams82-20UTE.8Po 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58724
|
2088
|
18
|
2026-05-19T13:31:24.244337+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779197484244_m1.jpg...
|
PhpStorm
|
faVsco.js – WebhookController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1D SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1DEV (docker)₴82APP (-zsl(use "git restore ‹file›..."to discard changesin working directory)modified:env.localmodified:app/Console/Commands/JiminnyDebugCommand.phpmodified:config/logging.phpUntracked files:Cuse "git add ‹file›..."to include in what will be committed).env.nikilocal.env.otherWEBHOOK_FILTERING_IMPLEMENTATION.mdapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.phpapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.phpids.txtpublic/favicon.icoraw_sql_query.saltests/Unit/Policies/CanAccessAiReportsTest.phpchanges added to commit (use "gitadd"and/or "git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pullremote: Enumerating objects: 63,remote: Counting objects: 100% (63/63), done.remote: Compressing objects: 100% (17/17),remote: Total 63 (delta 46), reused 63 (delta 46), pack-reused 0 (from 0)Unpacking objects: 100% (63/63), 6.51 KiB | 77.00 KiB/s, done.From github.com:jiminny/app16fa95720c..045ab2e1748a03ca9e03..84e7b10765JY-20842-remove-elastic-search-delete-documentsJY-20943-fix-calendar-eventsUpdating 5604af40cf..177ea29b8dorigin/JY-20origin/JY-20app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonroutes/api.phptests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phptests/Unit/Component/Transcription/Service/StorageServiceTest.php10 files changed, 82 insertions(+), 172 deletions(-)delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.phpdelete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20915-add-doma*Switched to a new branch 'JY-20915-add-domain-specific-email-text-relay'lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-emai• •ED→HomeDMsActivityFilesLater•*•MoreJiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of_jimi...• Direct messagesRio Nikolay Yankov. Galya Dimitrova Me. Vasil Vasilev®. Aneliya Angelovaa. Stefka Stoyanovao Stoyan TomovBs Todor Stamatov *Mario Georgiev. Nikolay Ivanovdo James GrahamStoyan TanevE Lukas Kovalik y... 0::: Appsi Jira CloudToast100% <Tue 19 May 16:31:23Describe what you are looking for# releases8 226 0• MessagesC Files• Bookmarks+177ea29b- Mequest #12102 fromTodayjiminny/JY-209~e un ur ai-call-scoring-accessjiminny/app | Added by GitHubCircleCI APP 3:00 PMDeployment Successful!Project: appWhen:05/19/202612:00:02Tag:View JobNewCircleCl APP3:35 PMNew commits deployed to Prophet Prod-US:(b1b41a2)(https://github.com/jiminny/prophet/commit/b1b41a207d8271f8f3128b3633b9b70597151ea2) -[JY-20919](https://jiminny.atlassian.net/browse/JY-20919): Lower exec reports batch size #518)(steliyan-g)New commits deployed to Prophet Prod-EU:[b1b41a2)(https://github.com/jiminny/prophet/commit/b1b41a207d8271f8f3128b3633b9b70597151ea2) -JY-20919](https://jiminny.atlassian.net/browse/JY-20919): Lower exec reports batch size (#518)(steliyan-g)Message #releases+Aa......
|
NULL
|
-1567704484429070502
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1D SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1DEV (docker)₴82APP (-zsl(use "git restore ‹file›..."to discard changesin working directory)modified:env.localmodified:app/Console/Commands/JiminnyDebugCommand.phpmodified:config/logging.phpUntracked files:Cuse "git add ‹file›..."to include in what will be committed).env.nikilocal.env.otherWEBHOOK_FILTERING_IMPLEMENTATION.mdapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.phpapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.phpids.txtpublic/favicon.icoraw_sql_query.saltests/Unit/Policies/CanAccessAiReportsTest.phpchanges added to commit (use "gitadd"and/or "git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pullremote: Enumerating objects: 63,remote: Counting objects: 100% (63/63), done.remote: Compressing objects: 100% (17/17),remote: Total 63 (delta 46), reused 63 (delta 46), pack-reused 0 (from 0)Unpacking objects: 100% (63/63), 6.51 KiB | 77.00 KiB/s, done.From github.com:jiminny/app16fa95720c..045ab2e1748a03ca9e03..84e7b10765JY-20842-remove-elastic-search-delete-documentsJY-20943-fix-calendar-eventsUpdating 5604af40cf..177ea29b8dorigin/JY-20origin/JY-20app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonroutes/api.phptests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phptests/Unit/Component/Transcription/Service/StorageServiceTest.php10 files changed, 82 insertions(+), 172 deletions(-)delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.phpdelete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20915-add-doma*Switched to a new branch 'JY-20915-add-domain-specific-email-text-relay'lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-emai• •ED→HomeDMsActivityFilesLater•*•MoreJiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of_jimi...• Direct messagesRio Nikolay Yankov. Galya Dimitrova Me. Vasil Vasilev®. Aneliya Angelovaa. Stefka Stoyanovao Stoyan TomovBs Todor Stamatov *Mario Georgiev. Nikolay Ivanovdo James GrahamStoyan TanevE Lukas Kovalik y... 0::: Appsi Jira CloudToast100% <Tue 19 May 16:31:23Describe what you are looking for# releases8 226 0• MessagesC Files• Bookmarks+177ea29b- Mequest #12102 fromTodayjiminny/JY-209~e un ur ai-call-scoring-accessjiminny/app | Added by GitHubCircleCI APP 3:00 PMDeployment Successful!Project: appWhen:05/19/202612:00:02Tag:View JobNewCircleCl APP3:35 PMNew commits deployed to Prophet Prod-US:(b1b41a2)(https://github.com/jiminny/prophet/commit/b1b41a207d8271f8f3128b3633b9b70597151ea2) -[JY-20919](https://jiminny.atlassian.net/browse/JY-20919): Lower exec reports batch size #518)(steliyan-g)New commits deployed to Prophet Prod-EU:[b1b41a2)(https://github.com/jiminny/prophet/commit/b1b41a207d8271f8f3128b3633b9b70597151ea2) -JY-20919](https://jiminny.atlassian.net/browse/JY-20919): Lower exec reports batch size (#518)(steliyan-g)Message #releases+Aa......
|
58722
|
NULL
|
NULL
|
NULL
|
|
58725
|
2088
|
19
|
2026-05-19T13:31:42.961267+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779197502961_m1.jpg...
|
PhpStorm
|
faVsco.js – WebhookController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20915-add-domain-speci Project: faVsco.js, menu
JY-20915-add-domain-specific-email-text-relay, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20915-add-domain-specific-email-text-relay, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20915-add-domain-specific-email-text-relay","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-6840606315183554619
|
-8780881095690048570
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20915-add-domain-speci Project: faVsco.js, menu
JY-20915-add-domain-specific-email-text-relay, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
SlackFileEditViewGoHistoryWindowHelpAPPDOCKER• ₴1DEV (docker)₴82APP (-zsl(use "git restore ‹file›..."to discard changesin working directory)modified:env.localmodified:app/Console/Commands/JiminnyDebugCommand.phpmodified:config/logging.phpUntracked files:Cuse "git add ‹file›..."to include in what will be committed).env.nikilocal.env.otherWEBHOOK_FILTERING_IMPLEMENTATION.mdapp/Console/Commands/Crm/Hubspot/SimulateWebhooksCommand.phpapp/Console/Commands/Reports/CreateMockAskJiminnyReportResultCommand.phpids.txtpublic/favicon.icoraw_sql_query.saltests/Unit/Policies/CanAccessAiReportsTest.phpchanges added to commit (use "gitadd"and/or "git commit -a")lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ git pullremote: Enumerating objects: 63,remote: Counting objects: 100% (63/63), done.remote: Compressing objects: 100% (17/17),remote: Total 63 (delta 46), reused 63 (delta 46), pack-reused 0 (from 0)Unpacking objects: 100% (63/63), 6.51 KiB | 77.00 KiB/s, done.From github.com:jiminny/app16fa95720c..045ab2e1748a03ca9e03..84e7b10765JY-20842-remove-elastic-search-delete-documentsJY-20943-fix-calendar-eventsUpdating 5604af40cf..177ea29b8dorigin/JY-20origin/JY-20app/Component/ES/ElasticSearchDocumentPartialUpdater.phpapp/Component/ParticipantSpeech/Services/ParticipantAttributionService.phpapp/Component/Settings/AutoScoring/Services/UpdateAutoScoreService.phpapp/Component/Transcription/Service/StorageService.phpfront-end/package.jsonroutes/api.phptests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phptests/Unit/Component/Settings/AutoScoring/Services/UpdateAutoScoreServiceTest.phptests/Unit/Component/Transcription/Service/StorageServiceTest.php10 files changed, 82 insertions(+), 172 deletions(-)delete mode 100644 app/Component/ES/ElasticSearchDocumentPartialUpdater.phpdelete mode 100644 tests/Unit/Component/ES/ElasticSearchDocumentPartialUpdaterTest.phplukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $ co -b JY-20915-add-doma*Switched to a new branch 'JY-20915-add-domain-specific-email-text-relay'lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-add-domain-specific-emai• •ED→HomeDMsActivityFilesLater•*•MoreJiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of_jimi...• Direct messagesRio Nikolay Yankov. Galya Dimitrova Me. Vasil Vasilev®. Aneliya Angelovaa. Stefka Stoyanovao Stoyan TomovBs Todor Stamatov *Mario Georgiev. Nikolay Ivanovdo James Graham8 Stoyan TanevE Lukas Kovalik y... 0::: Appsi Jira CloudToast100% <Tue 19 May 16:31:42Describe what you are looking for# releases8 226 0• MessagesC Files• Bookmarks+177ea29b- Mequest #12102 fromTodayjiminny/JY-209~e un ur ai-call-scoring-accessjiminny/app | Added by GitHubCircleCI APP 3:00 PMDeployment Successful!Project: appWhen:05/19/202612:00:02Tag:View JobNewCircleCl APP3:35 PMNew commits deployed to Prophet Prod-US:(b1b41a2)(https://github.com/jiminny/prophet/commit/b1b41a207d8271f8f3128b3633b9b70597151ea2) -[JY-20919](https://jiminny.atlassian.net/browse/JY-20919): Lower exec reports batch size #518)(steliyan-g)New commits deployed to Prophet Prod-EU:[b1b41a2)(https://github.com/jiminny/prophet/commit/b1b41a207d8271f8f3128b3633b9b70597151ea2) -JY-20919](https://jiminny.atlassian.net/browse/JY-20919): Lower exec reports batch size (#518)(steliyan-g)Message #releases+Aa......
|
NULL
|
NULL
|
NULL
|
NULL
|
|
58726
|
2089
|
19
|
2026-05-19T13:31:42.943132+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779197502943_m2.jpg...
|
PhpStorm
|
faVsco.js – WebhookController.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20915-add-domain-speci Project: faVsco.js, menu
JY-20915-add-domain-specific-email-text-relay, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
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":"JY-20915-add-domain-specific-email-text-relay, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11269947,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20915-add-domain-specific-email-text-relay","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7487819358851188460
|
-8708753132907942970
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20915-add-domain-speci Project: faVsco.js, menu
JY-20915-add-domain-specific-email-text-relay, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
PhostormcodeFV faVsco.js°9 JY-20915-add-domain-specifiProiect(e) MailboxController.ono©messagecontroller.onpg weonookcontroller.onp>D Voice) Audiocontroller.pnpc basecontroller.onov D WebhookHubspot›D IntearationAppsubscriptionsC) ActivityProviderController.phoC) ActivitvTranscriptioncontroller.phoc) BaseController.ohpP CalendarController.ohnc) Revortcontrolller.onvP SoftohoneWebhookController.ohpC AbstractController.oho© CommentContextinterface.php© ConferencesOptinOutController.php•) Controller nhn© ExportController.php© FrontendController.phpT FrontendControllerTrait.php8 GeocodingController.phpc) HealthCheckcontroller.ong92 liveCoachController.phpMissing leamcontroller.ong(e) MobileController.pnocNotricationcontroller.ono© NotificationProviderController.phpPlaybackController.php© PlaylistController.phpc) Pushercontroller.phpSlackController.php@ SupportController.phpc)Teamsetuocontroller.ono© UserAutomatedReportsController.phnC) WelcomeController.ohoMiddlewareRequestsM ResnonsasM Transformers(C) Kernel.nhnlclass WebhookController extends BaseControllerA17 81 .pUDLIc Tunculonrecervelexthessagzngservice snessagingservice.T PlaylistTrackResourceTrait.phpT. ValidateCrmConnectionRequiredTrait nhn.> M intearationsM IntoractionsvM lohe• MActivitvM AiAntomation$user =\Cache::remember($to,ttl: 3600, function • use ($to) {return user..wherel c'softphone_number', $to)->first0rFailO'7):// If we don't know who it is from, check the cache to see if we've seen that number mapped bef 575scachekey = "user. . suser-o1o scrino.prospecth . stromif (SprospectId === null && \Cache::has(ScacheKey)) {Sprospectid= Cache::getScacheKey)_ 580scachekey = 'user:' • suser->id_string • ":0bject-type#' • $from;1+ (sobnectivoe za= null o Lache:nos(scacheKev)) <sobnectivoe =cache:.det Scachekev):584$messagingService->setTeam(Suser->team):586587Sactivitv = Observer: skanEvents(588function () use (SmessagingService, $messageId, $user, $from, $to, $message, $prospectId, $589neturnSmescagingService-shuildActivitvdSmeccaneTd.type: Activity::TYPE_SMS_INBOUND,Susen$from,$to$message$prospectIdSobiectTypeF591592I593594-595596597scountrycodeWe have dererred Elastcsearch uodates. Let's uodate it noweventnew Uodates.ngleznttventitvld: Sactivity->aet1douodatelaraet: UodateTargetenum: ACTIVIry.purbose: 'telenhony-webhook-controller-receive'eventnew ReceivedSactivitv):59960060160260360460588888- 611Il Te nossible. nesnond with a cookie to add state. favouning the onnontunity oven the nrosnect 613if (Sactivitv-Sonnontunitv) {- 614Cookie: • queue ( narameters:telseif (Sactivitv-snnnsnect)'prospectid', $activity->opportunity-›crm_provider_id, 4 * 60):_615Mookie. • auotol narameters'prospectId', Sactivity->prospect->crm_provider_id, 4 * 60):Workcnace accociated with hranch 'macter' hac heon rectored Il Pollback II Confiaure (46 minutec aao)= laravel.log« SF jiminny@localhost]4 HS_local [jiminny@localhost]& console (PROD] X# console [euyA console [STAGING]CONCAT(U.Id, CASE WHEN U.Id = t.owner_Zd THEN ' (owner)" ELSE''END) AS user_1d -u.emar41 41 440 V.65 Asa.*t.owner_id FROM social_accounts saouin usens u on u.ide sa.sociable 1diOIN teams t .n<->l: on t.id = u.team_idHERE u.team_id = 581 and sa.provider = 'salesforce';ELECT * FROM automated_report_results order by id desc;elect * from features;elect * from team_features where feature_id = 40;elect * from teams where id = 556;elect * trom aucomated reports"here id = 54; # 4fdd41f6-dcf0-30d0-b339-7345381b6044,["pdf" "podcast"]ELECT * FROM automated report results WHERE uuid to bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid:elect * from automated_report_results order by id desc;ELECT * FROM automated report resultsWHERE id = 1919:elect * from automated report results WHERE report id = 54:elect * From opportunities where 1d = 75945491ELE * FROM teams WHERE name LIKE "%lesx: # 711. 692. 16067 -Timinnvintearationdlesmills.comelect * from playbooks where team_id = 711; # event 226147ELEc * FROM Dlavbook catedories WHERE DLavbook 1d = 55151ELECT * FROM crm_fields WHERE crm_configuration_id = 692 and object_type = 'event';ELECT * SROM eom fields WHEREI1d = 2261471ELECT * FROM crm_field_values WHERE crm_field_id = 226147;ELECT * FROM crm_configurations WHERE id = 692)SLSCTCONCAT(u.id, CASE WHEN u.id = t.owner_id THEN '(ownen)I FISE II EMO) AS ucen idnemailsa.*,t.owner id FROM social accounts saOIN users u on u.id = sa.sociable ioOIN teams t 1..n<->1: on t.id = u.team idHERE u.team id = 711 and sa.provider = 'salesforce':ELECT * FROM crm_profiles co JOIN users u 1..n<->1: on u.id = co.user_id WHERE u.team_id = 711:elect * from leadselect * from calendarsELECTT1d AS team 1d100% 5• Tue 19 May 16:31:42AskJiminnyReportActivityServiceTest vCascadeFix AutomatedReportsCascadeText Relay Email Routt+O •where is this "To send a message back., lust reoiv to this email. in the whole tlow? don't understand the wholeInougntror is?l'll search for the reoiv messade text and trace the comolete text relav flow to answer vour questions.searched lo send a message back in ~/iminny/appSearched reolv to this emaillin ~/iminnvlaools-received.blade.oho and EmailTextRelav.ohoThouaht for 1sThought for 3sSearched class Received in ~/iminnvlaooInoughttor ssInougnt tor zsThought for 2sText Relay Flow Explanation1. "To send a message back. just reoiv to this email " Location-received.blade. php (line 37)2. How Text Relay WorksPhase 1: Inbound SMS Received (Prospect → Jiminny User)• Prospect sends SMS to Jiminny user's softohone numberTwilio wehhook hits wehhookController:•receive/)l• Activity created with type TYPE SMS INBOUND• Received event fired (line 881• CreateReceivedNot ification listener sends email to userPhase 2. Emaill Notitication• SmsReceived notification sends email usina sms-received. blade. oho temolate• Email includes the message content and "To send a message back, just reply to this email.'Critical: The email has a special replvto addressAccent alliAsk anvthina (884-L)« Code SWF-1.6WN Windsurf Teams67-10 UTF.8io 4 spaces...
|
58723
|
NULL
|
NULL
|
NULL
|
|
58727
|
2088
|
20
|
2026-05-19T13:31:47.959558+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779197507959_m1.jpg...
|
PhpStorm
|
faVsco.js – WebhookController.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20915-add-domain-speci Project: faVsco.js, menu
JY-20915-add-domain-specific-email-text-relay, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
17
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers\Telephony\TextMessaging;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Contracts\ES\Events\UpdateSingleEntity;
use Jiminny\Contracts\ES\UpdateTargetEnum;
use Jiminny\Events\Activities\Sms\Received;
use Jiminny\Events\Activities\Sms\Delivered;
use Jiminny\Events\Activities\Sms\Sent;
use Jiminny\Events\Activities\Sms\Failed;
use Jiminny\Events\Activities\Sms\Undelivered;
use Jiminny\Models\User;
use Jiminny\Services\Telephony\TextMessagingService;
use Jiminny\Http\Controllers\Telephony\BaseController;
use Jiminny\Models\Activity;
class WebhookController extends BaseController
{
public const string EVENT_SMS_QUEUED = 'queued';
public const string EVENT_SMS_FAILED = 'failed';
public const string EVENT_SMS_SENT = 'sent';
public const string EVENT_SMS_DELIVERED = 'delivered';
public const string EVENT_SMS_UNDELIVERED = 'undelivered';
/**
* @return mixed
*/
public function receive(TextMessagingService $messagingService)
{
$this->validateWebhookRequest();
$from = $this->request->input('From');
$to = $this->request->input('To');
$message = $this->request->input('Body');
$prospectId = $this->request->cookie('prospectId');
$objectType = $this->request->cookie('objectType');
$messageId = $this->request->input('MessageSid');
// CNAM data.
$countryCode = $this->request->input('FromCountry') ?: null;
$user = \Cache::remember($to, 3600, function () use ($to) {
return User::where('softphone_number', $to)->firstOrFail();
});
// If we don't know who it is from, check the cache to see if we've seen that number mapped before.
$cacheKey = 'user:' . $user->id_string . ':prospect#' . $from;
if ($prospectId === null && \Cache::has($cacheKey)) {
$prospectId = \Cache::get($cacheKey);
}
$cacheKey = 'user:' . $user->id_string . ':object-type#' . $from;
if ($objectType === null && \Cache::has($cacheKey)) {
$objectType = \Cache::get($cacheKey);
}
$messagingService->setTeam($user->team);
$activity = Observer::skipEvents(
function () use ($messagingService, $messageId, $user, $from, $to, $message, $prospectId, $objectType, $countryCode) {
return $messagingService->buildActivity(
$messageId,
Activity::TYPE_SMS_INBOUND,
$user,
$from,
$to,
$message,
$prospectId,
$objectType,
$countryCode,
);
},
);
// We have deferred ElasticSearch updates. Let's update it now
event(new UpdateSingleEntity(
entityId: $activity->getId(),
updateTarget: UpdateTargetEnum::ACTIVITY,
purpose: 'telephony-webhook-controller-receive',
));
event(new Received($activity));
// If possible, respond with a cookie to add state, favouring the opportunity over the prospect.
if ($activity->opportunity) {
\Cookie::queue('prospectId', $activity->opportunity->crm_provider_id, 4 * 60);
} elseif ($activity->prospect) {
\Cookie::queue('prospectId', $activity->prospect->crm_provider_id, 4 * 60);
}
return response()->twiml();
}
private function fetchActivity(): ?Activity
{
return Activity::where('telephony_provider_id', $this->request->input('MessageSid'))->first();
}
public function event()
{
$this->validateWebhookRequest();
$status = $this->request->input('MessageStatus');
$errorCode = $this->request->input('ErrorCode');
$errorMessage = $this->request->input('ErrorMessage');
// Ignore the queued state since we don't use it.
if ($status === self::EVENT_SMS_QUEUED) {
return response()->twiml();
}
$activity = $this->fetchActivity();
if ($activity === null) {
// The webhook was too fast for us? Wait until we persist the remote ID to storage.
sleep(2);
$activity = $this->fetchActivity();
if ($activity === null) {
\Sentry::captureMessage($this->request->input('MessageSid') . ': webhook too slow.');
return response()->twiml();
}
}
// Transfer SMS events into our event stream.
switch ($status) {
case self::EVENT_SMS_FAILED:
$activity->update([
'status' => Activity::STATUS_FAILED,
]);
// See [URL_WITH_CREDENTIALS] -1)) AS calendar_domain
FROM teams t
JOIN users u ON u.team_id = t.id
JOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'
LEFT JOIN team_domains td
ON td.team_id = t.id
AND td.deleted_at IS NULL
AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))
GROUP BY t.id, t.name, calendar_domain
ORDER BY t.name, calendar_domain;
select * from users u join calendars c on c.user_id = u.id
where u.team_id = 882;
select * from activities where id = 74049485; # team 563 crm 537
select * from activities where id = 73272382; # team 563 crm 537
select * from activities where id = 64400389; # team 563 crm 537
select * from activities where id = 58081273; # team 563 crm 537
select * from activities where id = 54520297; # team 563 crm 537
select * from participants where activity_id = 58081273;
select * from activities where crm_configuration_id = 537 and provider = 'aircall'
and account_id = 19003658 order by updated_at desc;
select * from contacts where crm_configuration_id = 537 and id = 35957759;
select * from accounts where crm_configuration_id = 537 and id = 19003658;
select * from automated_report_results where id = 1976;
select * from automated_reports where id = 583;
select * from activity_searches where id = 87714;
select * from activity_search_filters where activity_search_id = 87714;
SELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid
or uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;
SELECT * FROM crm_configurations WHERE provider = 'hubspot';
select * from rate_limits;
select * from automated_report_results where media_type = 'pdf' and status = 2
and id IN (18, 1872);
select * from automated_reports where id = 54;
SELECT * FROM users WHERE id IN (24623,29443,29613);
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from text_relays where created_at > '2026-01-01';
and id IN (32415, 32416);
# and id = 32412;
select * from users where team_id = 2 and email like '%scott%' and id = 29510;
SELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436
SELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses
FROM text_relays
WHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')
GROUP BY email_provider_id;
SELECT id, status, telephony_provider_id, created_at
FROM activities
WHERE id IN (80028719, 80028846);
SELECT id, status, code, email_sent_at, created_at, updated_at
FROM text_relays
WHERE id IN (32415, 32416);
SELECT id, status, code, sender, recipient, created_at
FROM text_relays
WHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'
ORDER BY created_at DESC
LIMIT 10;
SELECT id, uuid, status, code, sender, recipient, created_at, updated_at
FROM text_relays
WHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');
# [PASSWORD_DOTS]
SELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count
FROM users u
INNER JOIN activities a ON u.id = a.user_id
WHERE a.type LIKE 'sms%'
AND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id, u.email, u.name, u.softphone_number
ORDER BY sms_count DESC;
select * from teams where id = 1;
select * from roles;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20915-add-domain-specific-email-text-relay, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20915-add-domain-specific-email-text-relay","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"17","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Http\\Controllers\\Telephony\\TextMessaging;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Contracts\\ES\\Events\\UpdateSingleEntity;\nuse Jiminny\\Contracts\\ES\\UpdateTargetEnum;\nuse Jiminny\\Events\\Activities\\Sms\\Received;\nuse Jiminny\\Events\\Activities\\Sms\\Delivered;\nuse Jiminny\\Events\\Activities\\Sms\\Sent;\nuse Jiminny\\Events\\Activities\\Sms\\Failed;\nuse Jiminny\\Events\\Activities\\Sms\\Undelivered;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Telephony\\TextMessagingService;\nuse Jiminny\\Http\\Controllers\\Telephony\\BaseController;\nuse Jiminny\\Models\\Activity;\n\nclass WebhookController extends BaseController\n{\n public const string EVENT_SMS_QUEUED = 'queued';\n\n public const string EVENT_SMS_FAILED = 'failed';\n\n public const string EVENT_SMS_SENT = 'sent';\n\n public const string EVENT_SMS_DELIVERED = 'delivered';\n\n public const string EVENT_SMS_UNDELIVERED = 'undelivered';\n\n /**\n * @return mixed\n */\n public function receive(TextMessagingService $messagingService)\n {\n $this->validateWebhookRequest();\n\n $from = $this->request->input('From');\n $to = $this->request->input('To');\n $message = $this->request->input('Body');\n $prospectId = $this->request->cookie('prospectId');\n $objectType = $this->request->cookie('objectType');\n $messageId = $this->request->input('MessageSid');\n\n // CNAM data.\n $countryCode = $this->request->input('FromCountry') ?: null;\n\n $user = \\Cache::remember($to, 3600, function () use ($to) {\n return User::where('softphone_number', $to)->firstOrFail();\n });\n\n // If we don't know who it is from, check the cache to see if we've seen that number mapped before.\n $cacheKey = 'user:' . $user->id_string . ':prospect#' . $from;\n if ($prospectId === null && \\Cache::has($cacheKey)) {\n $prospectId = \\Cache::get($cacheKey);\n }\n\n $cacheKey = 'user:' . $user->id_string . ':object-type#' . $from;\n if ($objectType === null && \\Cache::has($cacheKey)) {\n $objectType = \\Cache::get($cacheKey);\n }\n\n $messagingService->setTeam($user->team);\n\n $activity = Observer::skipEvents(\n function () use ($messagingService, $messageId, $user, $from, $to, $message, $prospectId, $objectType, $countryCode) {\n return $messagingService->buildActivity(\n $messageId,\n Activity::TYPE_SMS_INBOUND,\n $user,\n $from,\n $to,\n $message,\n $prospectId,\n $objectType,\n $countryCode,\n );\n },\n );\n\n // We have deferred ElasticSearch updates. Let's update it now\n event(new UpdateSingleEntity(\n entityId: $activity->getId(),\n updateTarget: UpdateTargetEnum::ACTIVITY,\n purpose: 'telephony-webhook-controller-receive',\n ));\n\n event(new Received($activity));\n\n // If possible, respond with a cookie to add state, favouring the opportunity over the prospect.\n if ($activity->opportunity) {\n \\Cookie::queue('prospectId', $activity->opportunity->crm_provider_id, 4 * 60);\n } elseif ($activity->prospect) {\n \\Cookie::queue('prospectId', $activity->prospect->crm_provider_id, 4 * 60);\n }\n\n return response()->twiml();\n }\n\n private function fetchActivity(): ?Activity\n {\n return Activity::where('telephony_provider_id', $this->request->input('MessageSid'))->first();\n }\n\n public function event()\n {\n $this->validateWebhookRequest();\n\n $status = $this->request->input('MessageStatus');\n $errorCode = $this->request->input('ErrorCode');\n $errorMessage = $this->request->input('ErrorMessage');\n\n // Ignore the queued state since we don't use it.\n if ($status === self::EVENT_SMS_QUEUED) {\n return response()->twiml();\n }\n\n $activity = $this->fetchActivity();\n\n if ($activity === null) {\n // The webhook was too fast for us? Wait until we persist the remote ID to storage.\n sleep(2);\n\n $activity = $this->fetchActivity();\n\n if ($activity === null) {\n \\Sentry::captureMessage($this->request->input('MessageSid') . ': webhook too slow.');\n\n return response()->twiml();\n }\n }\n\n // Transfer SMS events into our event stream.\n switch ($status) {\n case self::EVENT_SMS_FAILED:\n\n $activity->update([\n 'status' => Activity::STATUS_FAILED,\n ]);\n\n // See https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages\n event(new Failed($activity, $errorCode, $errorMessage));\n\n Datadog::increment('jiminny.sms.failed', 1, ['company' => $activity->user->team->slug]);\n\n break;\n\n case self::EVENT_SMS_SENT:\n\n $activity->update([\n 'status' => Activity::STATUS_SENT,\n 'is_processed' => true,\n ]);\n\n event(new Sent($activity));\n\n Datadog::increment('jiminny.sms.sent', 1, ['company' => $activity->user->team->slug]);\n\n break;\n\n case self::EVENT_SMS_DELIVERED:\n\n $activity->update([\n 'status' => Activity::STATUS_DELIVERED,\n ]);\n\n event(new Delivered($activity));\n\n Datadog::increment('jiminny.sms.delivered', 1, ['company' => $activity->user->team->slug]);\n\n break;\n\n case self::EVENT_SMS_UNDELIVERED:\n\n $activity->update([\n 'status' => Activity::STATUS_UNDELIVERED,\n ]);\n\n // See https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages\n event(new Undelivered($activity, $errorCode, $errorMessage));\n\n break;\n }\n\n return response()->twiml();\n }\n\n public function notification(): void\n {\n $this->validateWebhookRequest();\n\n $sid = $this->request->input('MessageSid');\n $status = $this->request->input('MessageStatus');\n\n if (\\in_array($status, [self::EVENT_SMS_FAILED, self::EVENT_SMS_UNDELIVERED], true)) {\n \\Log::info('An error occurred whilst sending sms message with sid ' . $sid . ' and status ' . $status);\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers\\Telephony\\TextMessaging;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Contracts\\ES\\Events\\UpdateSingleEntity;\nuse Jiminny\\Contracts\\ES\\UpdateTargetEnum;\nuse Jiminny\\Events\\Activities\\Sms\\Received;\nuse Jiminny\\Events\\Activities\\Sms\\Delivered;\nuse Jiminny\\Events\\Activities\\Sms\\Sent;\nuse Jiminny\\Events\\Activities\\Sms\\Failed;\nuse Jiminny\\Events\\Activities\\Sms\\Undelivered;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Telephony\\TextMessagingService;\nuse Jiminny\\Http\\Controllers\\Telephony\\BaseController;\nuse Jiminny\\Models\\Activity;\n\nclass WebhookController extends BaseController\n{\n public const string EVENT_SMS_QUEUED = 'queued';\n\n public const string EVENT_SMS_FAILED = 'failed';\n\n public const string EVENT_SMS_SENT = 'sent';\n\n public const string EVENT_SMS_DELIVERED = 'delivered';\n\n public const string EVENT_SMS_UNDELIVERED = 'undelivered';\n\n /**\n * @return mixed\n */\n public function receive(TextMessagingService $messagingService)\n {\n $this->validateWebhookRequest();\n\n $from = $this->request->input('From');\n $to = $this->request->input('To');\n $message = $this->request->input('Body');\n $prospectId = $this->request->cookie('prospectId');\n $objectType = $this->request->cookie('objectType');\n $messageId = $this->request->input('MessageSid');\n\n // CNAM data.\n $countryCode = $this->request->input('FromCountry') ?: null;\n\n $user = \\Cache::remember($to, 3600, function () use ($to) {\n return User::where('softphone_number', $to)->firstOrFail();\n });\n\n // If we don't know who it is from, check the cache to see if we've seen that number mapped before.\n $cacheKey = 'user:' . $user->id_string . ':prospect#' . $from;\n if ($prospectId === null && \\Cache::has($cacheKey)) {\n $prospectId = \\Cache::get($cacheKey);\n }\n\n $cacheKey = 'user:' . $user->id_string . ':object-type#' . $from;\n if ($objectType === null && \\Cache::has($cacheKey)) {\n $objectType = \\Cache::get($cacheKey);\n }\n\n $messagingService->setTeam($user->team);\n\n $activity = Observer::skipEvents(\n function () use ($messagingService, $messageId, $user, $from, $to, $message, $prospectId, $objectType, $countryCode) {\n return $messagingService->buildActivity(\n $messageId,\n Activity::TYPE_SMS_INBOUND,\n $user,\n $from,\n $to,\n $message,\n $prospectId,\n $objectType,\n $countryCode,\n );\n },\n );\n\n // We have deferred ElasticSearch updates. Let's update it now\n event(new UpdateSingleEntity(\n entityId: $activity->getId(),\n updateTarget: UpdateTargetEnum::ACTIVITY,\n purpose: 'telephony-webhook-controller-receive',\n ));\n\n event(new Received($activity));\n\n // If possible, respond with a cookie to add state, favouring the opportunity over the prospect.\n if ($activity->opportunity) {\n \\Cookie::queue('prospectId', $activity->opportunity->crm_provider_id, 4 * 60);\n } elseif ($activity->prospect) {\n \\Cookie::queue('prospectId', $activity->prospect->crm_provider_id, 4 * 60);\n }\n\n return response()->twiml();\n }\n\n private function fetchActivity(): ?Activity\n {\n return Activity::where('telephony_provider_id', $this->request->input('MessageSid'))->first();\n }\n\n public function event()\n {\n $this->validateWebhookRequest();\n\n $status = $this->request->input('MessageStatus');\n $errorCode = $this->request->input('ErrorCode');\n $errorMessage = $this->request->input('ErrorMessage');\n\n // Ignore the queued state since we don't use it.\n if ($status === self::EVENT_SMS_QUEUED) {\n return response()->twiml();\n }\n\n $activity = $this->fetchActivity();\n\n if ($activity === null) {\n // The webhook was too fast for us? Wait until we persist the remote ID to storage.\n sleep(2);\n\n $activity = $this->fetchActivity();\n\n if ($activity === null) {\n \\Sentry::captureMessage($this->request->input('MessageSid') . ': webhook too slow.');\n\n return response()->twiml();\n }\n }\n\n // Transfer SMS events into our event stream.\n switch ($status) {\n case self::EVENT_SMS_FAILED:\n\n $activity->update([\n 'status' => Activity::STATUS_FAILED,\n ]);\n\n // See https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages\n event(new Failed($activity, $errorCode, $errorMessage));\n\n Datadog::increment('jiminny.sms.failed', 1, ['company' => $activity->user->team->slug]);\n\n break;\n\n case self::EVENT_SMS_SENT:\n\n $activity->update([\n 'status' => Activity::STATUS_SENT,\n 'is_processed' => true,\n ]);\n\n event(new Sent($activity));\n\n Datadog::increment('jiminny.sms.sent', 1, ['company' => $activity->user->team->slug]);\n\n break;\n\n case self::EVENT_SMS_DELIVERED:\n\n $activity->update([\n 'status' => Activity::STATUS_DELIVERED,\n ]);\n\n event(new Delivered($activity));\n\n Datadog::increment('jiminny.sms.delivered', 1, ['company' => $activity->user->team->slug]);\n\n break;\n\n case self::EVENT_SMS_UNDELIVERED:\n\n $activity->update([\n 'status' => Activity::STATUS_UNDELIVERED,\n ]);\n\n // See https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages\n event(new Undelivered($activity, $errorCode, $errorMessage));\n\n break;\n }\n\n return response()->twiml();\n }\n\n public function notification(): void\n {\n $this->validateWebhookRequest();\n\n $sid = $this->request->input('MessageSid');\n $status = $this->request->input('MessageStatus');\n\n if (\\in_array($status, [self::EVENT_SMS_FAILED, self::EVENT_SMS_UNDELIVERED], true)) {\n \\Log::info('An error occurred whilst sending sms message with sid ' . $sid . ' and status ' . $status);\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"41","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"40","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"65","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT * FROM 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-01-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 = a.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;","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-01-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 = a.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;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-178182221919643202
|
2218652917440067141
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20915-add-domain-speci Project: faVsco.js, menu
JY-20915-add-domain-specific-email-text-relay, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
17
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers\Telephony\TextMessaging;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Contracts\ES\Events\UpdateSingleEntity;
use Jiminny\Contracts\ES\UpdateTargetEnum;
use Jiminny\Events\Activities\Sms\Received;
use Jiminny\Events\Activities\Sms\Delivered;
use Jiminny\Events\Activities\Sms\Sent;
use Jiminny\Events\Activities\Sms\Failed;
use Jiminny\Events\Activities\Sms\Undelivered;
use Jiminny\Models\User;
use Jiminny\Services\Telephony\TextMessagingService;
use Jiminny\Http\Controllers\Telephony\BaseController;
use Jiminny\Models\Activity;
class WebhookController extends BaseController
{
public const string EVENT_SMS_QUEUED = 'queued';
public const string EVENT_SMS_FAILED = 'failed';
public const string EVENT_SMS_SENT = 'sent';
public const string EVENT_SMS_DELIVERED = 'delivered';
public const string EVENT_SMS_UNDELIVERED = 'undelivered';
/**
* @return mixed
*/
public function receive(TextMessagingService $messagingService)
{
$this->validateWebhookRequest();
$from = $this->request->input('From');
$to = $this->request->input('To');
$message = $this->request->input('Body');
$prospectId = $this->request->cookie('prospectId');
$objectType = $this->request->cookie('objectType');
$messageId = $this->request->input('MessageSid');
// CNAM data.
$countryCode = $this->request->input('FromCountry') ?: null;
$user = \Cache::remember($to, 3600, function () use ($to) {
return User::where('softphone_number', $to)->firstOrFail();
});
// If we don't know who it is from, check the cache to see if we've seen that number mapped before.
$cacheKey = 'user:' . $user->id_string . ':prospect#' . $from;
if ($prospectId === null && \Cache::has($cacheKey)) {
$prospectId = \Cache::get($cacheKey);
}
$cacheKey = 'user:' . $user->id_string . ':object-type#' . $from;
if ($objectType === null && \Cache::has($cacheKey)) {
$objectType = \Cache::get($cacheKey);
}
$messagingService->setTeam($user->team);
$activity = Observer::skipEvents(
function () use ($messagingService, $messageId, $user, $from, $to, $message, $prospectId, $objectType, $countryCode) {
return $messagingService->buildActivity(
$messageId,
Activity::TYPE_SMS_INBOUND,
$user,
$from,
$to,
$message,
$prospectId,
$objectType,
$countryCode,
);
},
);
// We have deferred ElasticSearch updates. Let's update it now
event(new UpdateSingleEntity(
entityId: $activity->getId(),
updateTarget: UpdateTargetEnum::ACTIVITY,
purpose: 'telephony-webhook-controller-receive',
));
event(new Received($activity));
// If possible, respond with a cookie to add state, favouring the opportunity over the prospect.
if ($activity->opportunity) {
\Cookie::queue('prospectId', $activity->opportunity->crm_provider_id, 4 * 60);
} elseif ($activity->prospect) {
\Cookie::queue('prospectId', $activity->prospect->crm_provider_id, 4 * 60);
}
return response()->twiml();
}
private function fetchActivity(): ?Activity
{
return Activity::where('telephony_provider_id', $this->request->input('MessageSid'))->first();
}
public function event()
{
$this->validateWebhookRequest();
$status = $this->request->input('MessageStatus');
$errorCode = $this->request->input('ErrorCode');
$errorMessage = $this->request->input('ErrorMessage');
// Ignore the queued state since we don't use it.
if ($status === self::EVENT_SMS_QUEUED) {
return response()->twiml();
}
$activity = $this->fetchActivity();
if ($activity === null) {
// The webhook was too fast for us? Wait until we persist the remote ID to storage.
sleep(2);
$activity = $this->fetchActivity();
if ($activity === null) {
\Sentry::captureMessage($this->request->input('MessageSid') . ': webhook too slow.');
return response()->twiml();
}
}
// Transfer SMS events into our event stream.
switch ($status) {
case self::EVENT_SMS_FAILED:
$activity->update([
'status' => Activity::STATUS_FAILED,
]);
// See [URL_WITH_CREDENTIALS] -1)) AS calendar_domain
FROM teams t
JOIN users u ON u.team_id = t.id
JOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'
LEFT JOIN team_domains td
ON td.team_id = t.id
AND td.deleted_at IS NULL
AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))
GROUP BY t.id, t.name, calendar_domain
ORDER BY t.name, calendar_domain;
select * from users u join calendars c on c.user_id = u.id
where u.team_id = 882;
select * from activities where id = 74049485; # team 563 crm 537
select * from activities where id = 73272382; # team 563 crm 537
select * from activities where id = 64400389; # team 563 crm 537
select * from activities where id = 58081273; # team 563 crm 537
select * from activities where id = 54520297; # team 563 crm 537
select * from participants where activity_id = 58081273;
select * from activities where crm_configuration_id = 537 and provider = 'aircall'
and account_id = 19003658 order by updated_at desc;
select * from contacts where crm_configuration_id = 537 and id = 35957759;
select * from accounts where crm_configuration_id = 537 and id = 19003658;
select * from automated_report_results where id = 1976;
select * from automated_reports where id = 583;
select * from activity_searches where id = 87714;
select * from activity_search_filters where activity_search_id = 87714;
SELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid
or uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;
SELECT * FROM crm_configurations WHERE provider = 'hubspot';
select * from rate_limits;
select * from automated_report_results where media_type = 'pdf' and status = 2
and id IN (18, 1872);
select * from automated_reports where id = 54;
SELECT * FROM users WHERE id IN (24623,29443,29613);
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from text_relays where created_at > '2026-01-01';
and id IN (32415, 32416);
# and id = 32412;
select * from users where team_id = 2 and email like '%scott%' and id = 29510;
SELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436
SELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses
FROM text_relays
WHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')
GROUP BY email_provider_id;
SELECT id, status, telephony_provider_id, created_at
FROM activities
WHERE id IN (80028719, 80028846);
SELECT id, status, code, email_sent_at, created_at, updated_at
FROM text_relays
WHERE id IN (32415, 32416);
SELECT id, status, code, sender, recipient, created_at
FROM text_relays
WHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'
ORDER BY created_at DESC
LIMIT 10;
SELECT id, uuid, status, code, sender, recipient, created_at, updated_at
FROM text_relays
WHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');
# [PASSWORD_DOTS]
SELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count
FROM users u
INNER JOIN activities a ON u.id = a.user_id
WHERE a.type LIKE 'sms%'
AND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id, u.email, u.name, u.softphone_number
ORDER BY sms_count DESC;
select * from teams where id = 1;
select * from roles;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
58725
|
NULL
|
NULL
|
NULL
|
|
58728
|
2089
|
20
|
2026-05-19T13:31:47.959516+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-19/1779 /Users/lukas/.screenpipe/data/data/2026-05-19/1779197507959_m2.jpg...
|
PhpStorm
|
faVsco.js – WebhookController.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20915-add-domain-speci Project: faVsco.js, menu
JY-20915-add-domain-specific-email-text-relay, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
17
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers\Telephony\TextMessaging;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Contracts\ES\Events\UpdateSingleEntity;
use Jiminny\Contracts\ES\UpdateTargetEnum;
use Jiminny\Events\Activities\Sms\Received;
use Jiminny\Events\Activities\Sms\Delivered;
use Jiminny\Events\Activities\Sms\Sent;
use Jiminny\Events\Activities\Sms\Failed;
use Jiminny\Events\Activities\Sms\Undelivered;
use Jiminny\Models\User;
use Jiminny\Services\Telephony\TextMessagingService;
use Jiminny\Http\Controllers\Telephony\BaseController;
use Jiminny\Models\Activity;
class WebhookController extends BaseController
{
public const string EVENT_SMS_QUEUED = 'queued';
public const string EVENT_SMS_FAILED = 'failed';
public const string EVENT_SMS_SENT = 'sent';
public const string EVENT_SMS_DELIVERED = 'delivered';
public const string EVENT_SMS_UNDELIVERED = 'undelivered';
/**
* @return mixed
*/
public function receive(TextMessagingService $messagingService)
{
$this->validateWebhookRequest();
$from = $this->request->input('From');
$to = $this->request->input('To');
$message = $this->request->input('Body');
$prospectId = $this->request->cookie('prospectId');
$objectType = $this->request->cookie('objectType');
$messageId = $this->request->input('MessageSid');
// CNAM data.
$countryCode = $this->request->input('FromCountry') ?: null;
$user = \Cache::remember($to, 3600, function () use ($to) {
return User::where('softphone_number', $to)->firstOrFail();
});
// If we don't know who it is from, check the cache to see if we've seen that number mapped before.
$cacheKey = 'user:' . $user->id_string . ':prospect#' . $from;
if ($prospectId === null && \Cache::has($cacheKey)) {
$prospectId = \Cache::get($cacheKey);
}
$cacheKey = 'user:' . $user->id_string . ':object-type#' . $from;
if ($objectType === null && \Cache::has($cacheKey)) {
$objectType = \Cache::get($cacheKey);
}
$messagingService->setTeam($user->team);
$activity = Observer::skipEvents(
function () use ($messagingService, $messageId, $user, $from, $to, $message, $prospectId, $objectType, $countryCode) {
return $messagingService->buildActivity(
$messageId,
Activity::TYPE_SMS_INBOUND,
$user,
$from,
$to,
$message,
$prospectId,
$objectType,
$countryCode,
);
},
);
// We have deferred ElasticSearch updates. Let's update it now
event(new UpdateSingleEntity(
entityId: $activity->getId(),
updateTarget: UpdateTargetEnum::ACTIVITY,
purpose: 'telephony-webhook-controller-receive',
));
event(new Received($activity));
// If possible, respond with a cookie to add state, favouring the opportunity over the prospect.
if ($activity->opportunity) {
\Cookie::queue('prospectId', $activity->opportunity->crm_provider_id, 4 * 60);
} elseif ($activity->prospect) {
\Cookie::queue('prospectId', $activity->prospect->crm_provider_id, 4 * 60);
}
return response()->twiml();
}
private function fetchActivity(): ?Activity
{
return Activity::where('telephony_provider_id', $this->request->input('MessageSid'))->first();
}
public function event()
{
$this->validateWebhookRequest();
$status = $this->request->input('MessageStatus');
$errorCode = $this->request->input('ErrorCode');
$errorMessage = $this->request->input('ErrorMessage');
// Ignore the queued state since we don't use it.
if ($status === self::EVENT_SMS_QUEUED) {
return response()->twiml();
}
$activity = $this->fetchActivity();
if ($activity === null) {
// The webhook was too fast for us? Wait until we persist the remote ID to storage.
sleep(2);
$activity = $this->fetchActivity();
if ($activity === null) {
\Sentry::captureMessage($this->request->input('MessageSid') . ': webhook too slow.');
return response()->twiml();
}
}
// Transfer SMS events into our event stream.
switch ($status) {
case self::EVENT_SMS_FAILED:
$activity->update([
'status' => Activity::STATUS_FAILED,
]);
// See [URL_WITH_CREDENTIALS] -1)) AS calendar_domain
FROM teams t
JOIN users u ON u.team_id = t.id
JOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'
LEFT JOIN team_domains td
ON td.team_id = t.id
AND td.deleted_at IS NULL
AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))
GROUP BY t.id, t.name, calendar_domain
ORDER BY t.name, calendar_domain;
select * from users u join calendars c on c.user_id = u.id
where u.team_id = 882;
select * from activities where id = 74049485; # team 563 crm 537
select * from activities where id = 73272382; # team 563 crm 537
select * from activities where id = 64400389; # team 563 crm 537
select * from activities where id = 58081273; # team 563 crm 537
select * from activities where id = 54520297; # team 563 crm 537
select * from participants where activity_id = 58081273;
select * from activities where crm_configuration_id = 537 and provider = 'aircall'
and account_id = 19003658 order by updated_at desc;
select * from contacts where crm_configuration_id = 537 and id = 35957759;
select * from accounts where crm_configuration_id = 537 and id = 19003658;
select * from automated_report_results where id = 1976;
select * from automated_reports where id = 583;
select * from activity_searches where id = 87714;
select * from activity_search_filters where activity_search_id = 87714;
SELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid
or uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;
SELECT * FROM crm_configurations WHERE provider = 'hubspot';
select * from rate_limits;
select * from automated_report_results where media_type = 'pdf' and status = 2
and id IN (18, 1872);
select * from automated_reports where id = 54;
SELECT * FROM users WHERE id IN (24623,29443,29613);
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from text_relays where created_at > '2026-01-01';
and id IN (32415, 32416);
# and id = 32412;
select * from users where team_id = 2 and email like '%scott%' and id = 29510;
SELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436
SELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses
FROM text_relays
WHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')
GROUP BY email_provider_id;
SELECT id, status, telephony_provider_id, created_at
FROM activities
WHERE id IN (80028719, 80028846);
SELECT id, status, code, email_sent_at, created_at, updated_at
FROM text_relays
WHERE id IN (32415, 32416);
SELECT id, status, code, sender, recipient, created_at
FROM text_relays
WHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'
ORDER BY created_at DESC
LIMIT 10;
SELECT id, uuid, status, code, sender, recipient, created_at, updated_at
FROM text_relays
WHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');
# [PASSWORD_DOTS]
SELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count
FROM users u
INNER JOIN activities a ON u.id = a.user_id
WHERE a.type LIKE 'sms%'
AND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id, u.email, u.name, u.softphone_number
ORDER BY sms_count DESC;
select * from teams where id = 1;
select * from roles;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20915-add-domain-specific-email-text-relay, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.11269947,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20915-add-domain-specific-email-text-relay","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"17","depth":4,"bounds":{"left":0.39162233,"top":0.07581804,"width":0.00930851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.40292552,"top":0.07581804,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.4119016,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.4192154,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Http\\Controllers\\Telephony\\TextMessaging;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Contracts\\ES\\Events\\UpdateSingleEntity;\nuse Jiminny\\Contracts\\ES\\UpdateTargetEnum;\nuse Jiminny\\Events\\Activities\\Sms\\Received;\nuse Jiminny\\Events\\Activities\\Sms\\Delivered;\nuse Jiminny\\Events\\Activities\\Sms\\Sent;\nuse Jiminny\\Events\\Activities\\Sms\\Failed;\nuse Jiminny\\Events\\Activities\\Sms\\Undelivered;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Telephony\\TextMessagingService;\nuse Jiminny\\Http\\Controllers\\Telephony\\BaseController;\nuse Jiminny\\Models\\Activity;\n\nclass WebhookController extends BaseController\n{\n public const string EVENT_SMS_QUEUED = 'queued';\n\n public const string EVENT_SMS_FAILED = 'failed';\n\n public const string EVENT_SMS_SENT = 'sent';\n\n public const string EVENT_SMS_DELIVERED = 'delivered';\n\n public const string EVENT_SMS_UNDELIVERED = 'undelivered';\n\n /**\n * @return mixed\n */\n public function receive(TextMessagingService $messagingService)\n {\n $this->validateWebhookRequest();\n\n $from = $this->request->input('From');\n $to = $this->request->input('To');\n $message = $this->request->input('Body');\n $prospectId = $this->request->cookie('prospectId');\n $objectType = $this->request->cookie('objectType');\n $messageId = $this->request->input('MessageSid');\n\n // CNAM data.\n $countryCode = $this->request->input('FromCountry') ?: null;\n\n $user = \\Cache::remember($to, 3600, function () use ($to) {\n return User::where('softphone_number', $to)->firstOrFail();\n });\n\n // If we don't know who it is from, check the cache to see if we've seen that number mapped before.\n $cacheKey = 'user:' . $user->id_string . ':prospect#' . $from;\n if ($prospectId === null && \\Cache::has($cacheKey)) {\n $prospectId = \\Cache::get($cacheKey);\n }\n\n $cacheKey = 'user:' . $user->id_string . ':object-type#' . $from;\n if ($objectType === null && \\Cache::has($cacheKey)) {\n $objectType = \\Cache::get($cacheKey);\n }\n\n $messagingService->setTeam($user->team);\n\n $activity = Observer::skipEvents(\n function () use ($messagingService, $messageId, $user, $from, $to, $message, $prospectId, $objectType, $countryCode) {\n return $messagingService->buildActivity(\n $messageId,\n Activity::TYPE_SMS_INBOUND,\n $user,\n $from,\n $to,\n $message,\n $prospectId,\n $objectType,\n $countryCode,\n );\n },\n );\n\n // We have deferred ElasticSearch updates. Let's update it now\n event(new UpdateSingleEntity(\n entityId: $activity->getId(),\n updateTarget: UpdateTargetEnum::ACTIVITY,\n purpose: 'telephony-webhook-controller-receive',\n ));\n\n event(new Received($activity));\n\n // If possible, respond with a cookie to add state, favouring the opportunity over the prospect.\n if ($activity->opportunity) {\n \\Cookie::queue('prospectId', $activity->opportunity->crm_provider_id, 4 * 60);\n } elseif ($activity->prospect) {\n \\Cookie::queue('prospectId', $activity->prospect->crm_provider_id, 4 * 60);\n }\n\n return response()->twiml();\n }\n\n private function fetchActivity(): ?Activity\n {\n return Activity::where('telephony_provider_id', $this->request->input('MessageSid'))->first();\n }\n\n public function event()\n {\n $this->validateWebhookRequest();\n\n $status = $this->request->input('MessageStatus');\n $errorCode = $this->request->input('ErrorCode');\n $errorMessage = $this->request->input('ErrorMessage');\n\n // Ignore the queued state since we don't use it.\n if ($status === self::EVENT_SMS_QUEUED) {\n return response()->twiml();\n }\n\n $activity = $this->fetchActivity();\n\n if ($activity === null) {\n // The webhook was too fast for us? Wait until we persist the remote ID to storage.\n sleep(2);\n\n $activity = $this->fetchActivity();\n\n if ($activity === null) {\n \\Sentry::captureMessage($this->request->input('MessageSid') . ': webhook too slow.');\n\n return response()->twiml();\n }\n }\n\n // Transfer SMS events into our event stream.\n switch ($status) {\n case self::EVENT_SMS_FAILED:\n\n $activity->update([\n 'status' => Activity::STATUS_FAILED,\n ]);\n\n // See https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages\n event(new Failed($activity, $errorCode, $errorMessage));\n\n Datadog::increment('jiminny.sms.failed', 1, ['company' => $activity->user->team->slug]);\n\n break;\n\n case self::EVENT_SMS_SENT:\n\n $activity->update([\n 'status' => Activity::STATUS_SENT,\n 'is_processed' => true,\n ]);\n\n event(new Sent($activity));\n\n Datadog::increment('jiminny.sms.sent', 1, ['company' => $activity->user->team->slug]);\n\n break;\n\n case self::EVENT_SMS_DELIVERED:\n\n $activity->update([\n 'status' => Activity::STATUS_DELIVERED,\n ]);\n\n event(new Delivered($activity));\n\n Datadog::increment('jiminny.sms.delivered', 1, ['company' => $activity->user->team->slug]);\n\n break;\n\n case self::EVENT_SMS_UNDELIVERED:\n\n $activity->update([\n 'status' => Activity::STATUS_UNDELIVERED,\n ]);\n\n // See https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages\n event(new Undelivered($activity, $errorCode, $errorMessage));\n\n break;\n }\n\n return response()->twiml();\n }\n\n public function notification(): void\n {\n $this->validateWebhookRequest();\n\n $sid = $this->request->input('MessageSid');\n $status = $this->request->input('MessageStatus');\n\n if (\\in_array($status, [self::EVENT_SMS_FAILED, self::EVENT_SMS_UNDELIVERED], true)) {\n \\Log::info('An error occurred whilst sending sms message with sid ' . $sid . ' and status ' . $status);\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Http\\Controllers\\Telephony\\TextMessaging;\n\nuse ChaseConey\\LaravelDatadogHelper\\Datadog;\nuse Jiminny\\Component\\ElasticSearch\\Model\\Observer;\nuse Jiminny\\Contracts\\ES\\Events\\UpdateSingleEntity;\nuse Jiminny\\Contracts\\ES\\UpdateTargetEnum;\nuse Jiminny\\Events\\Activities\\Sms\\Received;\nuse Jiminny\\Events\\Activities\\Sms\\Delivered;\nuse Jiminny\\Events\\Activities\\Sms\\Sent;\nuse Jiminny\\Events\\Activities\\Sms\\Failed;\nuse Jiminny\\Events\\Activities\\Sms\\Undelivered;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Telephony\\TextMessagingService;\nuse Jiminny\\Http\\Controllers\\Telephony\\BaseController;\nuse Jiminny\\Models\\Activity;\n\nclass WebhookController extends BaseController\n{\n public const string EVENT_SMS_QUEUED = 'queued';\n\n public const string EVENT_SMS_FAILED = 'failed';\n\n public const string EVENT_SMS_SENT = 'sent';\n\n public const string EVENT_SMS_DELIVERED = 'delivered';\n\n public const string EVENT_SMS_UNDELIVERED = 'undelivered';\n\n /**\n * @return mixed\n */\n public function receive(TextMessagingService $messagingService)\n {\n $this->validateWebhookRequest();\n\n $from = $this->request->input('From');\n $to = $this->request->input('To');\n $message = $this->request->input('Body');\n $prospectId = $this->request->cookie('prospectId');\n $objectType = $this->request->cookie('objectType');\n $messageId = $this->request->input('MessageSid');\n\n // CNAM data.\n $countryCode = $this->request->input('FromCountry') ?: null;\n\n $user = \\Cache::remember($to, 3600, function () use ($to) {\n return User::where('softphone_number', $to)->firstOrFail();\n });\n\n // If we don't know who it is from, check the cache to see if we've seen that number mapped before.\n $cacheKey = 'user:' . $user->id_string . ':prospect#' . $from;\n if ($prospectId === null && \\Cache::has($cacheKey)) {\n $prospectId = \\Cache::get($cacheKey);\n }\n\n $cacheKey = 'user:' . $user->id_string . ':object-type#' . $from;\n if ($objectType === null && \\Cache::has($cacheKey)) {\n $objectType = \\Cache::get($cacheKey);\n }\n\n $messagingService->setTeam($user->team);\n\n $activity = Observer::skipEvents(\n function () use ($messagingService, $messageId, $user, $from, $to, $message, $prospectId, $objectType, $countryCode) {\n return $messagingService->buildActivity(\n $messageId,\n Activity::TYPE_SMS_INBOUND,\n $user,\n $from,\n $to,\n $message,\n $prospectId,\n $objectType,\n $countryCode,\n );\n },\n );\n\n // We have deferred ElasticSearch updates. Let's update it now\n event(new UpdateSingleEntity(\n entityId: $activity->getId(),\n updateTarget: UpdateTargetEnum::ACTIVITY,\n purpose: 'telephony-webhook-controller-receive',\n ));\n\n event(new Received($activity));\n\n // If possible, respond with a cookie to add state, favouring the opportunity over the prospect.\n if ($activity->opportunity) {\n \\Cookie::queue('prospectId', $activity->opportunity->crm_provider_id, 4 * 60);\n } elseif ($activity->prospect) {\n \\Cookie::queue('prospectId', $activity->prospect->crm_provider_id, 4 * 60);\n }\n\n return response()->twiml();\n }\n\n private function fetchActivity(): ?Activity\n {\n return Activity::where('telephony_provider_id', $this->request->input('MessageSid'))->first();\n }\n\n public function event()\n {\n $this->validateWebhookRequest();\n\n $status = $this->request->input('MessageStatus');\n $errorCode = $this->request->input('ErrorCode');\n $errorMessage = $this->request->input('ErrorMessage');\n\n // Ignore the queued state since we don't use it.\n if ($status === self::EVENT_SMS_QUEUED) {\n return response()->twiml();\n }\n\n $activity = $this->fetchActivity();\n\n if ($activity === null) {\n // The webhook was too fast for us? Wait until we persist the remote ID to storage.\n sleep(2);\n\n $activity = $this->fetchActivity();\n\n if ($activity === null) {\n \\Sentry::captureMessage($this->request->input('MessageSid') . ': webhook too slow.');\n\n return response()->twiml();\n }\n }\n\n // Transfer SMS events into our event stream.\n switch ($status) {\n case self::EVENT_SMS_FAILED:\n\n $activity->update([\n 'status' => Activity::STATUS_FAILED,\n ]);\n\n // See https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages\n event(new Failed($activity, $errorCode, $errorMessage));\n\n Datadog::increment('jiminny.sms.failed', 1, ['company' => $activity->user->team->slug]);\n\n break;\n\n case self::EVENT_SMS_SENT:\n\n $activity->update([\n 'status' => Activity::STATUS_SENT,\n 'is_processed' => true,\n ]);\n\n event(new Sent($activity));\n\n Datadog::increment('jiminny.sms.sent', 1, ['company' => $activity->user->team->slug]);\n\n break;\n\n case self::EVENT_SMS_DELIVERED:\n\n $activity->update([\n 'status' => Activity::STATUS_DELIVERED,\n ]);\n\n event(new Delivered($activity));\n\n Datadog::increment('jiminny.sms.delivered', 1, ['company' => $activity->user->team->slug]);\n\n break;\n\n case self::EVENT_SMS_UNDELIVERED:\n\n $activity->update([\n 'status' => Activity::STATUS_UNDELIVERED,\n ]);\n\n // See https://support.twilio.com/hc/en-us/articles/223181868-Troubleshooting-undelivered-Twilio-SMS-messages\n event(new Undelivered($activity, $errorCode, $errorMessage));\n\n break;\n }\n\n return response()->twiml();\n }\n\n public function notification(): void\n {\n $this->validateWebhookRequest();\n\n $sid = $this->request->input('MessageSid');\n $status = $this->request->input('MessageStatus');\n\n if (\\in_array($status, [self::EVENT_SMS_FAILED, self::EVENT_SMS_UNDELIVERED], true)) {\n \\Log::info('An error occurred whilst sending sms message with sid ' . $sid . ' and status ' . $status);\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"41","depth":4,"bounds":{"left":0.6761968,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.6878325,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"40","depth":4,"bounds":{"left":0.69714093,"top":0.10055866,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"65","depth":4,"bounds":{"left":0.7094415,"top":0.10055866,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.72140956,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7287234,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"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-01-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 = a.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;","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-01-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 = a.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;","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}]...
|
-178182221919643202
|
2218652917440067141
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20915-add-domain-speci Project: faVsco.js, menu
JY-20915-add-domain-specific-email-text-relay, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
17
1
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Http\Controllers\Telephony\TextMessaging;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Jiminny\Component\ElasticSearch\Model\Observer;
use Jiminny\Contracts\ES\Events\UpdateSingleEntity;
use Jiminny\Contracts\ES\UpdateTargetEnum;
use Jiminny\Events\Activities\Sms\Received;
use Jiminny\Events\Activities\Sms\Delivered;
use Jiminny\Events\Activities\Sms\Sent;
use Jiminny\Events\Activities\Sms\Failed;
use Jiminny\Events\Activities\Sms\Undelivered;
use Jiminny\Models\User;
use Jiminny\Services\Telephony\TextMessagingService;
use Jiminny\Http\Controllers\Telephony\BaseController;
use Jiminny\Models\Activity;
class WebhookController extends BaseController
{
public const string EVENT_SMS_QUEUED = 'queued';
public const string EVENT_SMS_FAILED = 'failed';
public const string EVENT_SMS_SENT = 'sent';
public const string EVENT_SMS_DELIVERED = 'delivered';
public const string EVENT_SMS_UNDELIVERED = 'undelivered';
/**
* @return mixed
*/
public function receive(TextMessagingService $messagingService)
{
$this->validateWebhookRequest();
$from = $this->request->input('From');
$to = $this->request->input('To');
$message = $this->request->input('Body');
$prospectId = $this->request->cookie('prospectId');
$objectType = $this->request->cookie('objectType');
$messageId = $this->request->input('MessageSid');
// CNAM data.
$countryCode = $this->request->input('FromCountry') ?: null;
$user = \Cache::remember($to, 3600, function () use ($to) {
return User::where('softphone_number', $to)->firstOrFail();
});
// If we don't know who it is from, check the cache to see if we've seen that number mapped before.
$cacheKey = 'user:' . $user->id_string . ':prospect#' . $from;
if ($prospectId === null && \Cache::has($cacheKey)) {
$prospectId = \Cache::get($cacheKey);
}
$cacheKey = 'user:' . $user->id_string . ':object-type#' . $from;
if ($objectType === null && \Cache::has($cacheKey)) {
$objectType = \Cache::get($cacheKey);
}
$messagingService->setTeam($user->team);
$activity = Observer::skipEvents(
function () use ($messagingService, $messageId, $user, $from, $to, $message, $prospectId, $objectType, $countryCode) {
return $messagingService->buildActivity(
$messageId,
Activity::TYPE_SMS_INBOUND,
$user,
$from,
$to,
$message,
$prospectId,
$objectType,
$countryCode,
);
},
);
// We have deferred ElasticSearch updates. Let's update it now
event(new UpdateSingleEntity(
entityId: $activity->getId(),
updateTarget: UpdateTargetEnum::ACTIVITY,
purpose: 'telephony-webhook-controller-receive',
));
event(new Received($activity));
// If possible, respond with a cookie to add state, favouring the opportunity over the prospect.
if ($activity->opportunity) {
\Cookie::queue('prospectId', $activity->opportunity->crm_provider_id, 4 * 60);
} elseif ($activity->prospect) {
\Cookie::queue('prospectId', $activity->prospect->crm_provider_id, 4 * 60);
}
return response()->twiml();
}
private function fetchActivity(): ?Activity
{
return Activity::where('telephony_provider_id', $this->request->input('MessageSid'))->first();
}
public function event()
{
$this->validateWebhookRequest();
$status = $this->request->input('MessageStatus');
$errorCode = $this->request->input('ErrorCode');
$errorMessage = $this->request->input('ErrorMessage');
// Ignore the queued state since we don't use it.
if ($status === self::EVENT_SMS_QUEUED) {
return response()->twiml();
}
$activity = $this->fetchActivity();
if ($activity === null) {
// The webhook was too fast for us? Wait until we persist the remote ID to storage.
sleep(2);
$activity = $this->fetchActivity();
if ($activity === null) {
\Sentry::captureMessage($this->request->input('MessageSid') . ': webhook too slow.');
return response()->twiml();
}
}
// Transfer SMS events into our event stream.
switch ($status) {
case self::EVENT_SMS_FAILED:
$activity->update([
'status' => Activity::STATUS_FAILED,
]);
// See [URL_WITH_CREDENTIALS] -1)) AS calendar_domain
FROM teams t
JOIN users u ON u.team_id = t.id
JOIN calendars c ON c.user_id = u.id AND c.status = 'active' AND c.calendar_provider_id LIKE '%@%'
LEFT JOIN team_domains td
ON td.team_id = t.id
AND td.deleted_at IS NULL
AND td.domain = LOWER(SUBSTRING_INDEX(c.calendar_provider_id, '@', -1))
GROUP BY t.id, t.name, calendar_domain
ORDER BY t.name, calendar_domain;
select * from users u join calendars c on c.user_id = u.id
where u.team_id = 882;
select * from activities where id = 74049485; # team 563 crm 537
select * from activities where id = 73272382; # team 563 crm 537
select * from activities where id = 64400389; # team 563 crm 537
select * from activities where id = 58081273; # team 563 crm 537
select * from activities where id = 54520297; # team 563 crm 537
select * from participants where activity_id = 58081273;
select * from activities where crm_configuration_id = 537 and provider = 'aircall'
and account_id = 19003658 order by updated_at desc;
select * from contacts where crm_configuration_id = 537 and id = 35957759;
select * from accounts where crm_configuration_id = 537 and id = 19003658;
select * from automated_report_results where id = 1976;
select * from automated_reports where id = 583;
select * from activity_searches where id = 87714;
select * from activity_search_filters where activity_search_id = 87714;
SELECT * FROM activities WHERE uuid_to_bin('8827f672-202d-4162-9d04-73ff5f0566a9') = uuid
or uuid_to_bin('47842446-af51-4bcb-854f-cc6560290101') = uuid;
SELECT * FROM crm_configurations WHERE provider = 'hubspot';
select * from rate_limits;
select * from automated_report_results where media_type = 'pdf' and status = 2
and id IN (18, 1872);
select * from automated_reports where id = 54;
SELECT * FROM users WHERE id IN (24623,29443,29613);
SELECT * FROM automated_report_results WHERE uuid_to_bin('822fa41b-afd3-43a9-a248-86b0e36f3131') = uuid;
select * from text_relays where created_at > '2026-01-01';
and id IN (32415, 32416);
# and id = 32412;
select * from users where team_id = 2 and email like '%scott%' and id = 29510;
SELECT * FROM activities WHERE uuid_to_bin('67cebfc2-ed56-44a2-8c68-7a0286ed8618') = uuid; # 79763436
SELECT email_provider_id, COUNT(*) as count, GROUP_CONCAT(id) as ids, GROUP_CONCAT(status) as statuses
FROM text_relays
WHERE email_provider_id IN ('19e2027868a64b42', '19e2033ed8ea6b10')
GROUP BY email_provider_id;
SELECT id, status, telephony_provider_id, created_at
FROM activities
WHERE id IN (80028719, 80028846);
SELECT id, status, code, email_sent_at, created_at, updated_at
FROM text_relays
WHERE id IN (32415, 32416);
SELECT id, status, code, sender, recipient, created_at
FROM text_relays
WHERE sender LIKE '%mario.georgiev%' OR sender LIKE '%stoyan.tomov%'
ORDER BY created_at DESC
LIMIT 10;
SELECT id, uuid, status, code, sender, recipient, created_at, updated_at
FROM text_relays
WHERE uuid = uuid_to_bin('0626141c-27a6-4d8c-aff8-7c8020a2c656');
# [PASSWORD_DOTS]
SELECT DISTINCT u.id, u.email, u.name, u.softphone_number, COUNT(a.id) as sms_count
FROM users u
INNER JOIN activities a ON u.id = a.user_id
WHERE a.type LIKE 'sms%'
AND a.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id, u.email, u.name, u.softphone_number
ORDER BY sms_count DESC;
select * from teams where id = 1;
select * from roles;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2770
|
113
|
1
|
2026-05-07T11:39:37.580111+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153977580_m1.jpg...
|
PhpStorm
|
faVsco.js – WebhookSyncBatchProcessor.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use Illuminate\Support\Facades\Log;
use Jiminny\Jobs\Crm\Hubspot\ImportAccountBatch;
use Jiminny\Jobs\Crm\Hubspot\ImportContactBatch;
use Jiminny\Jobs\Crm\Hubspot\ImportOpportunityBatch;
/**
* Processes batches of HubSpot objects collected from webhook events.
*
* This service drains Redis sets containing CRM IDs that were collected from webhook
* property change and association change events. It dispatches Import*Batch jobs to
* process the objects in batches of 100.
*
* Design Decisions:
*
* 1. Redis Service Injection:
* BatchSyncRedisService is injected via constructor for testability and to avoid
* tight coupling to Redis facade. This allows easy mocking in tests.
*
* 2. Multi-Object Type Support:
* processBatchesForObjectType() directly with their specific object type.
*
* 3. Batch Size and Limits:
* - BATCH_SIZE = 100: HubSpot batch API limit
* - MAX_BATCHES = 1000: Safety limit to prevent infinite loops (100K objects max per run)
*
* 4. Failure Handling:
* If job dispatch fails, IDs are requeued to Redis for the next processing cycle.
* Individual job failures are handled by the Import*Batch jobs themselves.
*/
class WebhookSyncBatchProcessor
{
public const string OBJECT_TYPE_DEAL = 'deal';
public const string OBJECT_TYPE_CONTACT = 'contact';
public const string OBJECT_TYPE_COMPANY = 'company';
private const int BATCH_SIZE = 100;
private const int MAX_BATCHES = 1000;
private const array SUPPORTED_OBJECT_TYPES = [
self::OBJECT_TYPE_DEAL,
self::OBJECT_TYPE_CONTACT,
self::OBJECT_TYPE_COMPANY,
];
public function __construct(
private BatchSyncRedisService $redisService
) {
}
/**
* Process batches for a specific object type.
*
* Drains the Redis set for the given object type and config, dispatching Import*Batch
* jobs in batches of 100 until the set is empty or MAX_BATCHES is reached.
*
* @param string $objectType One of: 'deal', 'contact', 'company'
* @param int $configId CRM configuration ID
*
* @return int Total number of IDs dispatched to jobs
*/
public function processBatchesForObjectType(string $objectType, int $configId): int
{
if (! in_array($objectType, self::SUPPORTED_OBJECT_TYPES, true)) {
Log::warning('[WebhookSyncBatchProcessor] Unsupported object type', [
'object_type' => $objectType,
'config_id' => $configId,
]);
return 0;
}
$processedCount = 0;
$batchNumber = 0;
while ($batchNumber < self::MAX_BATCHES) {
try {
$crmProviderIds = $this->redisService->getCollectedObjects(
$objectType,
$configId,
'all',
self::BATCH_SIZE
);
} catch (\Throwable $e) {
Log::error('[WebhookSyncBatchProcessor] Failed to fetch IDs from Redis', [
'object_type' => $objectType,
'config_id' => $configId,
'batch_number' => $batchNumber,
'error' => $e->getMessage(),
]);
break;
}
if (empty($crmProviderIds)) {
break;
}
try {
$this->dispatchBatchJob($objectType, $configId, $crmProviderIds);
$processedCount += \count($crmProviderIds);
$batchNumber++;
} catch (\Throwable $e) {
Log::error('[WebhookSyncBatchProcessor] Batch dispatch failed, stopping', [
'object_type' => $objectType,
'config_id' => $configId,
'batch_number' => $batchNumber,
'batch_size' => \count($crmProviderIds),
'error' => $e->getMessage(),
]);
$this->requeueFailedIds($objectType, $configId, $crmProviderIds);
break;
}
}
if ($batchNumber >= self::MAX_BATCHES) {
Log::warning('[WebhookSyncBatchProcessor] Maximum batch limit reached', [
'object_type' => $objectType,
'config_id' => $configId,
'max_batches' => self::MAX_BATCHES,
'total_dispatched' => $processedCount,
]);
}
if ($processedCount > 0) {
Log::info('[WebhookSyncBatchProcessor] Batch processing completed', [
'object_type' => $objectType,
'config_id' => $configId,
'total_dispatched' => $processedCount,
'batches_dispatched' => $batchNumber,
]);
}
return $processedCount;
}
private function dispatchBatchJob(string $objectType, int $configId, array $crmProviderIds): void
{
match ($objectType) {
self::OBJECT_TYPE_DEAL => ImportOpportunityBatch::dispatch($configId, $crmProviderIds),
self::OBJECT_TYPE_CONTACT => ImportContactBatch::dispatch($configId, $crmProviderIds),
self::OBJECT_TYPE_COMPANY => ImportAccountBatch::dispatch($configId, $crmProviderIds),
};
}
private function requeueFailedIds(string $objectType, int $configId, array $crmProviderIds): void
{
$succeeded = 0;
$permanentlyFailed = [];
foreach ($crmProviderIds as $crmProviderId) {
try {
$this->redisService->collectObjectIdToBatch(
$objectType,
$crmProviderId,
$configId,
'all'
);
$succeeded++;
} catch (\Throwable $idError) {
$permanentlyFailed[] = $crmProviderId;
Log::warning('[WebhookSyncBatchProcessor] Failed to requeue ID', [
'object_type' => $objectType,
'config_id' => $configId,
'crm_provider_id' => $crmProviderId,
'error' => $idError->getMessage(),
]);
}
}
Log::info('[WebhookSyncBatchProcessor] Requeue completed', [
'object_type' => $objectType,
'config_id' => $configId,
'succeeded' => $succeeded,
'failed' => \count($permanentlyFailed),
'total' => \count($crmProviderIds),
]);
if (! empty($permanentlyFailed)) {
Log::error('[WebhookSyncBatchProcessor] Permanently failed to requeue IDs', [
'object_type' => $objectType,
'config_id' => $configId,
'failed_ids' => $permanentlyFailed,
]);
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Jobs\\Crm\\Hubspot\\ImportAccountBatch;\nuse Jiminny\\Jobs\\Crm\\Hubspot\\ImportContactBatch;\nuse Jiminny\\Jobs\\Crm\\Hubspot\\ImportOpportunityBatch;\n\n/**\n * Processes batches of HubSpot objects collected from webhook events.\n *\n * This service drains Redis sets containing CRM IDs that were collected from webhook\n * property change and association change events. It dispatches Import*Batch jobs to\n * process the objects in batches of 100.\n *\n * Design Decisions:\n *\n * 1. Redis Service Injection:\n * BatchSyncRedisService is injected via constructor for testability and to avoid\n * tight coupling to Redis facade. This allows easy mocking in tests.\n *\n * 2. Multi-Object Type Support:\n * processBatchesForObjectType() directly with their specific object type.\n *\n * 3. Batch Size and Limits:\n * - BATCH_SIZE = 100: HubSpot batch API limit\n * - MAX_BATCHES = 1000: Safety limit to prevent infinite loops (100K objects max per run)\n *\n * 4. Failure Handling:\n * If job dispatch fails, IDs are requeued to Redis for the next processing cycle.\n * Individual job failures are handled by the Import*Batch jobs themselves.\n */\nclass WebhookSyncBatchProcessor\n{\n public const string OBJECT_TYPE_DEAL = 'deal';\n public const string OBJECT_TYPE_CONTACT = 'contact';\n public const string OBJECT_TYPE_COMPANY = 'company';\n private const int BATCH_SIZE = 100;\n private const int MAX_BATCHES = 1000;\n\n private const array SUPPORTED_OBJECT_TYPES = [\n self::OBJECT_TYPE_DEAL,\n self::OBJECT_TYPE_CONTACT,\n self::OBJECT_TYPE_COMPANY,\n ];\n\n public function __construct(\n private BatchSyncRedisService $redisService\n ) {\n }\n\n /**\n * Process batches for a specific object type.\n *\n * Drains the Redis set for the given object type and config, dispatching Import*Batch\n * jobs in batches of 100 until the set is empty or MAX_BATCHES is reached.\n *\n * @param string $objectType One of: 'deal', 'contact', 'company'\n * @param int $configId CRM configuration ID\n *\n * @return int Total number of IDs dispatched to jobs\n */\n public function processBatchesForObjectType(string $objectType, int $configId): int\n {\n if (! in_array($objectType, self::SUPPORTED_OBJECT_TYPES, true)) {\n Log::warning('[WebhookSyncBatchProcessor] Unsupported object type', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n ]);\n\n return 0;\n }\n\n $processedCount = 0;\n $batchNumber = 0;\n\n while ($batchNumber < self::MAX_BATCHES) {\n try {\n $crmProviderIds = $this->redisService->getCollectedObjects(\n $objectType,\n $configId,\n 'all',\n self::BATCH_SIZE\n );\n } catch (\\Throwable $e) {\n Log::error('[WebhookSyncBatchProcessor] Failed to fetch IDs from Redis', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'batch_number' => $batchNumber,\n 'error' => $e->getMessage(),\n ]);\n\n break;\n }\n\n if (empty($crmProviderIds)) {\n break;\n }\n\n try {\n $this->dispatchBatchJob($objectType, $configId, $crmProviderIds);\n\n $processedCount += \\count($crmProviderIds);\n $batchNumber++;\n } catch (\\Throwable $e) {\n Log::error('[WebhookSyncBatchProcessor] Batch dispatch failed, stopping', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'batch_number' => $batchNumber,\n 'batch_size' => \\count($crmProviderIds),\n 'error' => $e->getMessage(),\n ]);\n\n $this->requeueFailedIds($objectType, $configId, $crmProviderIds);\n\n break;\n }\n }\n\n if ($batchNumber >= self::MAX_BATCHES) {\n Log::warning('[WebhookSyncBatchProcessor] Maximum batch limit reached', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'max_batches' => self::MAX_BATCHES,\n 'total_dispatched' => $processedCount,\n ]);\n }\n\n if ($processedCount > 0) {\n Log::info('[WebhookSyncBatchProcessor] Batch processing completed', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'total_dispatched' => $processedCount,\n 'batches_dispatched' => $batchNumber,\n ]);\n }\n\n return $processedCount;\n }\n\n private function dispatchBatchJob(string $objectType, int $configId, array $crmProviderIds): void\n {\n match ($objectType) {\n self::OBJECT_TYPE_DEAL => ImportOpportunityBatch::dispatch($configId, $crmProviderIds),\n self::OBJECT_TYPE_CONTACT => ImportContactBatch::dispatch($configId, $crmProviderIds),\n self::OBJECT_TYPE_COMPANY => ImportAccountBatch::dispatch($configId, $crmProviderIds),\n };\n }\n\n private function requeueFailedIds(string $objectType, int $configId, array $crmProviderIds): void\n {\n $succeeded = 0;\n $permanentlyFailed = [];\n\n foreach ($crmProviderIds as $crmProviderId) {\n try {\n $this->redisService->collectObjectIdToBatch(\n $objectType,\n $crmProviderId,\n $configId,\n 'all'\n );\n $succeeded++;\n } catch (\\Throwable $idError) {\n $permanentlyFailed[] = $crmProviderId;\n Log::warning('[WebhookSyncBatchProcessor] Failed to requeue ID', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'crm_provider_id' => $crmProviderId,\n 'error' => $idError->getMessage(),\n ]);\n }\n }\n\n Log::info('[WebhookSyncBatchProcessor] Requeue completed', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'succeeded' => $succeeded,\n 'failed' => \\count($permanentlyFailed),\n 'total' => \\count($crmProviderIds),\n ]);\n\n if (! empty($permanentlyFailed)) {\n Log::error('[WebhookSyncBatchProcessor] Permanently failed to requeue IDs', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'failed_ids' => $permanentlyFailed,\n ]);\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Jobs\\Crm\\Hubspot\\ImportAccountBatch;\nuse Jiminny\\Jobs\\Crm\\Hubspot\\ImportContactBatch;\nuse Jiminny\\Jobs\\Crm\\Hubspot\\ImportOpportunityBatch;\n\n/**\n * Processes batches of HubSpot objects collected from webhook events.\n *\n * This service drains Redis sets containing CRM IDs that were collected from webhook\n * property change and association change events. It dispatches Import*Batch jobs to\n * process the objects in batches of 100.\n *\n * Design Decisions:\n *\n * 1. Redis Service Injection:\n * BatchSyncRedisService is injected via constructor for testability and to avoid\n * tight coupling to Redis facade. This allows easy mocking in tests.\n *\n * 2. Multi-Object Type Support:\n * processBatchesForObjectType() directly with their specific object type.\n *\n * 3. Batch Size and Limits:\n * - BATCH_SIZE = 100: HubSpot batch API limit\n * - MAX_BATCHES = 1000: Safety limit to prevent infinite loops (100K objects max per run)\n *\n * 4. Failure Handling:\n * If job dispatch fails, IDs are requeued to Redis for the next processing cycle.\n * Individual job failures are handled by the Import*Batch jobs themselves.\n */\nclass WebhookSyncBatchProcessor\n{\n public const string OBJECT_TYPE_DEAL = 'deal';\n public const string OBJECT_TYPE_CONTACT = 'contact';\n public const string OBJECT_TYPE_COMPANY = 'company';\n private const int BATCH_SIZE = 100;\n private const int MAX_BATCHES = 1000;\n\n private const array SUPPORTED_OBJECT_TYPES = [\n self::OBJECT_TYPE_DEAL,\n self::OBJECT_TYPE_CONTACT,\n self::OBJECT_TYPE_COMPANY,\n ];\n\n public function __construct(\n private BatchSyncRedisService $redisService\n ) {\n }\n\n /**\n * Process batches for a specific object type.\n *\n * Drains the Redis set for the given object type and config, dispatching Import*Batch\n * jobs in batches of 100 until the set is empty or MAX_BATCHES is reached.\n *\n * @param string $objectType One of: 'deal', 'contact', 'company'\n * @param int $configId CRM configuration ID\n *\n * @return int Total number of IDs dispatched to jobs\n */\n public function processBatchesForObjectType(string $objectType, int $configId): int\n {\n if (! in_array($objectType, self::SUPPORTED_OBJECT_TYPES, true)) {\n Log::warning('[WebhookSyncBatchProcessor] Unsupported object type', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n ]);\n\n return 0;\n }\n\n $processedCount = 0;\n $batchNumber = 0;\n\n while ($batchNumber < self::MAX_BATCHES) {\n try {\n $crmProviderIds = $this->redisService->getCollectedObjects(\n $objectType,\n $configId,\n 'all',\n self::BATCH_SIZE\n );\n } catch (\\Throwable $e) {\n Log::error('[WebhookSyncBatchProcessor] Failed to fetch IDs from Redis', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'batch_number' => $batchNumber,\n 'error' => $e->getMessage(),\n ]);\n\n break;\n }\n\n if (empty($crmProviderIds)) {\n break;\n }\n\n try {\n $this->dispatchBatchJob($objectType, $configId, $crmProviderIds);\n\n $processedCount += \\count($crmProviderIds);\n $batchNumber++;\n } catch (\\Throwable $e) {\n Log::error('[WebhookSyncBatchProcessor] Batch dispatch failed, stopping', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'batch_number' => $batchNumber,\n 'batch_size' => \\count($crmProviderIds),\n 'error' => $e->getMessage(),\n ]);\n\n $this->requeueFailedIds($objectType, $configId, $crmProviderIds);\n\n break;\n }\n }\n\n if ($batchNumber >= self::MAX_BATCHES) {\n Log::warning('[WebhookSyncBatchProcessor] Maximum batch limit reached', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'max_batches' => self::MAX_BATCHES,\n 'total_dispatched' => $processedCount,\n ]);\n }\n\n if ($processedCount > 0) {\n Log::info('[WebhookSyncBatchProcessor] Batch processing completed', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'total_dispatched' => $processedCount,\n 'batches_dispatched' => $batchNumber,\n ]);\n }\n\n return $processedCount;\n }\n\n private function dispatchBatchJob(string $objectType, int $configId, array $crmProviderIds): void\n {\n match ($objectType) {\n self::OBJECT_TYPE_DEAL => ImportOpportunityBatch::dispatch($configId, $crmProviderIds),\n self::OBJECT_TYPE_CONTACT => ImportContactBatch::dispatch($configId, $crmProviderIds),\n self::OBJECT_TYPE_COMPANY => ImportAccountBatch::dispatch($configId, $crmProviderIds),\n };\n }\n\n private function requeueFailedIds(string $objectType, int $configId, array $crmProviderIds): void\n {\n $succeeded = 0;\n $permanentlyFailed = [];\n\n foreach ($crmProviderIds as $crmProviderId) {\n try {\n $this->redisService->collectObjectIdToBatch(\n $objectType,\n $crmProviderId,\n $configId,\n 'all'\n );\n $succeeded++;\n } catch (\\Throwable $idError) {\n $permanentlyFailed[] = $crmProviderId;\n Log::warning('[WebhookSyncBatchProcessor] Failed to requeue ID', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'crm_provider_id' => $crmProviderId,\n 'error' => $idError->getMessage(),\n ]);\n }\n }\n\n Log::info('[WebhookSyncBatchProcessor] Requeue completed', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'succeeded' => $succeeded,\n 'failed' => \\count($permanentlyFailed),\n 'total' => \\count($crmProviderIds),\n ]);\n\n if (! empty($permanentlyFailed)) {\n Log::error('[WebhookSyncBatchProcessor] Permanently failed to requeue IDs', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'failed_ids' => $permanentlyFailed,\n ]);\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-8010415956189026930
|
-8883631698440123185
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use Illuminate\Support\Facades\Log;
use Jiminny\Jobs\Crm\Hubspot\ImportAccountBatch;
use Jiminny\Jobs\Crm\Hubspot\ImportContactBatch;
use Jiminny\Jobs\Crm\Hubspot\ImportOpportunityBatch;
/**
* Processes batches of HubSpot objects collected from webhook events.
*
* This service drains Redis sets containing CRM IDs that were collected from webhook
* property change and association change events. It dispatches Import*Batch jobs to
* process the objects in batches of 100.
*
* Design Decisions:
*
* 1. Redis Service Injection:
* BatchSyncRedisService is injected via constructor for testability and to avoid
* tight coupling to Redis facade. This allows easy mocking in tests.
*
* 2. Multi-Object Type Support:
* processBatchesForObjectType() directly with their specific object type.
*
* 3. Batch Size and Limits:
* - BATCH_SIZE = 100: HubSpot batch API limit
* - MAX_BATCHES = 1000: Safety limit to prevent infinite loops (100K objects max per run)
*
* 4. Failure Handling:
* If job dispatch fails, IDs are requeued to Redis for the next processing cycle.
* Individual job failures are handled by the Import*Batch jobs themselves.
*/
class WebhookSyncBatchProcessor
{
public const string OBJECT_TYPE_DEAL = 'deal';
public const string OBJECT_TYPE_CONTACT = 'contact';
public const string OBJECT_TYPE_COMPANY = 'company';
private const int BATCH_SIZE = 100;
private const int MAX_BATCHES = 1000;
private const array SUPPORTED_OBJECT_TYPES = [
self::OBJECT_TYPE_DEAL,
self::OBJECT_TYPE_CONTACT,
self::OBJECT_TYPE_COMPANY,
];
public function __construct(
private BatchSyncRedisService $redisService
) {
}
/**
* Process batches for a specific object type.
*
* Drains the Redis set for the given object type and config, dispatching Import*Batch
* jobs in batches of 100 until the set is empty or MAX_BATCHES is reached.
*
* @param string $objectType One of: 'deal', 'contact', 'company'
* @param int $configId CRM configuration ID
*
* @return int Total number of IDs dispatched to jobs
*/
public function processBatchesForObjectType(string $objectType, int $configId): int
{
if (! in_array($objectType, self::SUPPORTED_OBJECT_TYPES, true)) {
Log::warning('[WebhookSyncBatchProcessor] Unsupported object type', [
'object_type' => $objectType,
'config_id' => $configId,
]);
return 0;
}
$processedCount = 0;
$batchNumber = 0;
while ($batchNumber < self::MAX_BATCHES) {
try {
$crmProviderIds = $this->redisService->getCollectedObjects(
$objectType,
$configId,
'all',
self::BATCH_SIZE
);
} catch (\Throwable $e) {
Log::error('[WebhookSyncBatchProcessor] Failed to fetch IDs from Redis', [
'object_type' => $objectType,
'config_id' => $configId,
'batch_number' => $batchNumber,
'error' => $e->getMessage(),
]);
break;
}
if (empty($crmProviderIds)) {
break;
}
try {
$this->dispatchBatchJob($objectType, $configId, $crmProviderIds);
$processedCount += \count($crmProviderIds);
$batchNumber++;
} catch (\Throwable $e) {
Log::error('[WebhookSyncBatchProcessor] Batch dispatch failed, stopping', [
'object_type' => $objectType,
'config_id' => $configId,
'batch_number' => $batchNumber,
'batch_size' => \count($crmProviderIds),
'error' => $e->getMessage(),
]);
$this->requeueFailedIds($objectType, $configId, $crmProviderIds);
break;
}
}
if ($batchNumber >= self::MAX_BATCHES) {
Log::warning('[WebhookSyncBatchProcessor] Maximum batch limit reached', [
'object_type' => $objectType,
'config_id' => $configId,
'max_batches' => self::MAX_BATCHES,
'total_dispatched' => $processedCount,
]);
}
if ($processedCount > 0) {
Log::info('[WebhookSyncBatchProcessor] Batch processing completed', [
'object_type' => $objectType,
'config_id' => $configId,
'total_dispatched' => $processedCount,
'batches_dispatched' => $batchNumber,
]);
}
return $processedCount;
}
private function dispatchBatchJob(string $objectType, int $configId, array $crmProviderIds): void
{
match ($objectType) {
self::OBJECT_TYPE_DEAL => ImportOpportunityBatch::dispatch($configId, $crmProviderIds),
self::OBJECT_TYPE_CONTACT => ImportContactBatch::dispatch($configId, $crmProviderIds),
self::OBJECT_TYPE_COMPANY => ImportAccountBatch::dispatch($configId, $crmProviderIds),
};
}
private function requeueFailedIds(string $objectType, int $configId, array $crmProviderIds): void
{
$succeeded = 0;
$permanentlyFailed = [];
foreach ($crmProviderIds as $crmProviderId) {
try {
$this->redisService->collectObjectIdToBatch(
$objectType,
$crmProviderId,
$configId,
'all'
);
$succeeded++;
} catch (\Throwable $idError) {
$permanentlyFailed[] = $crmProviderId;
Log::warning('[WebhookSyncBatchProcessor] Failed to requeue ID', [
'object_type' => $objectType,
'config_id' => $configId,
'crm_provider_id' => $crmProviderId,
'error' => $idError->getMessage(),
]);
}
}
Log::info('[WebhookSyncBatchProcessor] Requeue completed', [
'object_type' => $objectType,
'config_id' => $configId,
'succeeded' => $succeeded,
'failed' => \count($permanentlyFailed),
'total' => \count($crmProviderIds),
]);
if (! empty($permanentlyFailed)) {
Log::error('[WebhookSyncBatchProcessor] Permanently failed to requeue IDs', [
'object_type' => $objectType,
'config_id' => $configId,
'failed_ids' => $permanentlyFailed,
]);
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
2768
|
NULL
|
NULL
|
NULL
|
|
2771
|
114
|
1
|
2026-05-07T11:39:37.580002+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153977580_m2.jpg...
|
PhpStorm
|
faVsco.js – WebhookSyncBatchProcessor.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use Illuminate\Support\Facades\Log;
use Jiminny\Jobs\Crm\Hubspot\ImportAccountBatch;
use Jiminny\Jobs\Crm\Hubspot\ImportContactBatch;
use Jiminny\Jobs\Crm\Hubspot\ImportOpportunityBatch;
/**
* Processes batches of HubSpot objects collected from webhook events.
*
* This service drains Redis sets containing CRM IDs that were collected from webhook
* property change and association change events. It dispatches Import*Batch jobs to
* process the objects in batches of 100.
*
* Design Decisions:
*
* 1. Redis Service Injection:
* BatchSyncRedisService is injected via constructor for testability and to avoid
* tight coupling to Redis facade. This allows easy mocking in tests.
*
* 2. Multi-Object Type Support:
* processBatchesForObjectType() directly with their specific object type.
*
* 3. Batch Size and Limits:
* - BATCH_SIZE = 100: HubSpot batch API limit
* - MAX_BATCHES = 1000: Safety limit to prevent infinite loops (100K objects max per run)
*
* 4. Failure Handling:
* If job dispatch fails, IDs are requeued to Redis for the next processing cycle.
* Individual job failures are handled by the Import*Batch jobs themselves.
*/
class WebhookSyncBatchProcessor
{
public const string OBJECT_TYPE_DEAL = 'deal';
public const string OBJECT_TYPE_CONTACT = 'contact';
public const string OBJECT_TYPE_COMPANY = 'company';
private const int BATCH_SIZE = 100;
private const int MAX_BATCHES = 1000;
private const array SUPPORTED_OBJECT_TYPES = [
self::OBJECT_TYPE_DEAL,
self::OBJECT_TYPE_CONTACT,
self::OBJECT_TYPE_COMPANY,
];
public function __construct(
private BatchSyncRedisService $redisService
) {
}
/**
* Process batches for a specific object type.
*
* Drains the Redis set for the given object type and config, dispatching Import*Batch
* jobs in batches of 100 until the set is empty or MAX_BATCHES is reached.
*
* @param string $objectType One of: 'deal', 'contact', 'company'
* @param int $configId CRM configuration ID
*
* @return int Total number of IDs dispatched to jobs
*/
public function processBatchesForObjectType(string $objectType, int $configId): int
{
if (! in_array($objectType, self::SUPPORTED_OBJECT_TYPES, true)) {
Log::warning('[WebhookSyncBatchProcessor] Unsupported object type', [
'object_type' => $objectType,
'config_id' => $configId,
]);
return 0;
}
$processedCount = 0;
$batchNumber = 0;
while ($batchNumber < self::MAX_BATCHES) {
try {
$crmProviderIds = $this->redisService->getCollectedObjects(
$objectType,
$configId,
'all',
self::BATCH_SIZE
);
} catch (\Throwable $e) {
Log::error('[WebhookSyncBatchProcessor] Failed to fetch IDs from Redis', [
'object_type' => $objectType,
'config_id' => $configId,
'batch_number' => $batchNumber,
'error' => $e->getMessage(),
]);
break;
}
if (empty($crmProviderIds)) {
break;
}
try {
$this->dispatchBatchJob($objectType, $configId, $crmProviderIds);
$processedCount += \count($crmProviderIds);
$batchNumber++;
} catch (\Throwable $e) {
Log::error('[WebhookSyncBatchProcessor] Batch dispatch failed, stopping', [
'object_type' => $objectType,
'config_id' => $configId,
'batch_number' => $batchNumber,
'batch_size' => \count($crmProviderIds),
'error' => $e->getMessage(),
]);
$this->requeueFailedIds($objectType, $configId, $crmProviderIds);
break;
}
}
if ($batchNumber >= self::MAX_BATCHES) {
Log::warning('[WebhookSyncBatchProcessor] Maximum batch limit reached', [
'object_type' => $objectType,
'config_id' => $configId,
'max_batches' => self::MAX_BATCHES,
'total_dispatched' => $processedCount,
]);
}
if ($processedCount > 0) {
Log::info('[WebhookSyncBatchProcessor] Batch processing completed', [
'object_type' => $objectType,
'config_id' => $configId,
'total_dispatched' => $processedCount,
'batches_dispatched' => $batchNumber,
]);
}
return $processedCount;
}
private function dispatchBatchJob(string $objectType, int $configId, array $crmProviderIds): void
{
match ($objectType) {
self::OBJECT_TYPE_DEAL => ImportOpportunityBatch::dispatch($configId, $crmProviderIds),
self::OBJECT_TYPE_CONTACT => ImportContactBatch::dispatch($configId, $crmProviderIds),
self::OBJECT_TYPE_COMPANY => ImportAccountBatch::dispatch($configId, $crmProviderIds),
};
}
private function requeueFailedIds(string $objectType, int $configId, array $crmProviderIds): void
{
$succeeded = 0;
$permanentlyFailed = [];
foreach ($crmProviderIds as $crmProviderId) {
try {
$this->redisService->collectObjectIdToBatch(
$objectType,
$crmProviderId,
$configId,
'all'
);
$succeeded++;
} catch (\Throwable $idError) {
$permanentlyFailed[] = $crmProviderId;
Log::warning('[WebhookSyncBatchProcessor] Failed to requeue ID', [
'object_type' => $objectType,
'config_id' => $configId,
'crm_provider_id' => $crmProviderId,
'error' => $idError->getMessage(),
]);
}
}
Log::info('[WebhookSyncBatchProcessor] Requeue completed', [
'object_type' => $objectType,
'config_id' => $configId,
'succeeded' => $succeeded,
'failed' => \count($permanentlyFailed),
'total' => \count($crmProviderIds),
]);
if (! empty($permanentlyFailed)) {
Log::error('[WebhookSyncBatchProcessor] Permanently failed to requeue IDs', [
'object_type' => $objectType,
'config_id' => $configId,
'failed_ids' => $permanentlyFailed,
]);
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"bounds":{"left":0.5475399,"top":0.0726257,"width":0.44082448,"height":0.9066241},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"bounds":{"left":0.4950133,"top":0.17478053,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.5049867,"top":0.17478053,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.51396275,"top":0.17318435,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.5212766,"top":0.17318435,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Jobs\\Crm\\Hubspot\\ImportAccountBatch;\nuse Jiminny\\Jobs\\Crm\\Hubspot\\ImportContactBatch;\nuse Jiminny\\Jobs\\Crm\\Hubspot\\ImportOpportunityBatch;\n\n/**\n * Processes batches of HubSpot objects collected from webhook events.\n *\n * This service drains Redis sets containing CRM IDs that were collected from webhook\n * property change and association change events. It dispatches Import*Batch jobs to\n * process the objects in batches of 100.\n *\n * Design Decisions:\n *\n * 1. Redis Service Injection:\n * BatchSyncRedisService is injected via constructor for testability and to avoid\n * tight coupling to Redis facade. This allows easy mocking in tests.\n *\n * 2. Multi-Object Type Support:\n * processBatchesForObjectType() directly with their specific object type.\n *\n * 3. Batch Size and Limits:\n * - BATCH_SIZE = 100: HubSpot batch API limit\n * - MAX_BATCHES = 1000: Safety limit to prevent infinite loops (100K objects max per run)\n *\n * 4. Failure Handling:\n * If job dispatch fails, IDs are requeued to Redis for the next processing cycle.\n * Individual job failures are handled by the Import*Batch jobs themselves.\n */\nclass WebhookSyncBatchProcessor\n{\n public const string OBJECT_TYPE_DEAL = 'deal';\n public const string OBJECT_TYPE_CONTACT = 'contact';\n public const string OBJECT_TYPE_COMPANY = 'company';\n private const int BATCH_SIZE = 100;\n private const int MAX_BATCHES = 1000;\n\n private const array SUPPORTED_OBJECT_TYPES = [\n self::OBJECT_TYPE_DEAL,\n self::OBJECT_TYPE_CONTACT,\n self::OBJECT_TYPE_COMPANY,\n ];\n\n public function __construct(\n private BatchSyncRedisService $redisService\n ) {\n }\n\n /**\n * Process batches for a specific object type.\n *\n * Drains the Redis set for the given object type and config, dispatching Import*Batch\n * jobs in batches of 100 until the set is empty or MAX_BATCHES is reached.\n *\n * @param string $objectType One of: 'deal', 'contact', 'company'\n * @param int $configId CRM configuration ID\n *\n * @return int Total number of IDs dispatched to jobs\n */\n public function processBatchesForObjectType(string $objectType, int $configId): int\n {\n if (! in_array($objectType, self::SUPPORTED_OBJECT_TYPES, true)) {\n Log::warning('[WebhookSyncBatchProcessor] Unsupported object type', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n ]);\n\n return 0;\n }\n\n $processedCount = 0;\n $batchNumber = 0;\n\n while ($batchNumber < self::MAX_BATCHES) {\n try {\n $crmProviderIds = $this->redisService->getCollectedObjects(\n $objectType,\n $configId,\n 'all',\n self::BATCH_SIZE\n );\n } catch (\\Throwable $e) {\n Log::error('[WebhookSyncBatchProcessor] Failed to fetch IDs from Redis', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'batch_number' => $batchNumber,\n 'error' => $e->getMessage(),\n ]);\n\n break;\n }\n\n if (empty($crmProviderIds)) {\n break;\n }\n\n try {\n $this->dispatchBatchJob($objectType, $configId, $crmProviderIds);\n\n $processedCount += \\count($crmProviderIds);\n $batchNumber++;\n } catch (\\Throwable $e) {\n Log::error('[WebhookSyncBatchProcessor] Batch dispatch failed, stopping', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'batch_number' => $batchNumber,\n 'batch_size' => \\count($crmProviderIds),\n 'error' => $e->getMessage(),\n ]);\n\n $this->requeueFailedIds($objectType, $configId, $crmProviderIds);\n\n break;\n }\n }\n\n if ($batchNumber >= self::MAX_BATCHES) {\n Log::warning('[WebhookSyncBatchProcessor] Maximum batch limit reached', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'max_batches' => self::MAX_BATCHES,\n 'total_dispatched' => $processedCount,\n ]);\n }\n\n if ($processedCount > 0) {\n Log::info('[WebhookSyncBatchProcessor] Batch processing completed', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'total_dispatched' => $processedCount,\n 'batches_dispatched' => $batchNumber,\n ]);\n }\n\n return $processedCount;\n }\n\n private function dispatchBatchJob(string $objectType, int $configId, array $crmProviderIds): void\n {\n match ($objectType) {\n self::OBJECT_TYPE_DEAL => ImportOpportunityBatch::dispatch($configId, $crmProviderIds),\n self::OBJECT_TYPE_CONTACT => ImportContactBatch::dispatch($configId, $crmProviderIds),\n self::OBJECT_TYPE_COMPANY => ImportAccountBatch::dispatch($configId, $crmProviderIds),\n };\n }\n\n private function requeueFailedIds(string $objectType, int $configId, array $crmProviderIds): void\n {\n $succeeded = 0;\n $permanentlyFailed = [];\n\n foreach ($crmProviderIds as $crmProviderId) {\n try {\n $this->redisService->collectObjectIdToBatch(\n $objectType,\n $crmProviderId,\n $configId,\n 'all'\n );\n $succeeded++;\n } catch (\\Throwable $idError) {\n $permanentlyFailed[] = $crmProviderId;\n Log::warning('[WebhookSyncBatchProcessor] Failed to requeue ID', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'crm_provider_id' => $crmProviderId,\n 'error' => $idError->getMessage(),\n ]);\n }\n }\n\n Log::info('[WebhookSyncBatchProcessor] Requeue completed', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'succeeded' => $succeeded,\n 'failed' => \\count($permanentlyFailed),\n 'total' => \\count($crmProviderIds),\n ]);\n\n if (! empty($permanentlyFailed)) {\n Log::error('[WebhookSyncBatchProcessor] Permanently failed to requeue IDs', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'failed_ids' => $permanentlyFailed,\n ]);\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Jobs\\Crm\\Hubspot\\ImportAccountBatch;\nuse Jiminny\\Jobs\\Crm\\Hubspot\\ImportContactBatch;\nuse Jiminny\\Jobs\\Crm\\Hubspot\\ImportOpportunityBatch;\n\n/**\n * Processes batches of HubSpot objects collected from webhook events.\n *\n * This service drains Redis sets containing CRM IDs that were collected from webhook\n * property change and association change events. It dispatches Import*Batch jobs to\n * process the objects in batches of 100.\n *\n * Design Decisions:\n *\n * 1. Redis Service Injection:\n * BatchSyncRedisService is injected via constructor for testability and to avoid\n * tight coupling to Redis facade. This allows easy mocking in tests.\n *\n * 2. Multi-Object Type Support:\n * processBatchesForObjectType() directly with their specific object type.\n *\n * 3. Batch Size and Limits:\n * - BATCH_SIZE = 100: HubSpot batch API limit\n * - MAX_BATCHES = 1000: Safety limit to prevent infinite loops (100K objects max per run)\n *\n * 4. Failure Handling:\n * If job dispatch fails, IDs are requeued to Redis for the next processing cycle.\n * Individual job failures are handled by the Import*Batch jobs themselves.\n */\nclass WebhookSyncBatchProcessor\n{\n public const string OBJECT_TYPE_DEAL = 'deal';\n public const string OBJECT_TYPE_CONTACT = 'contact';\n public const string OBJECT_TYPE_COMPANY = 'company';\n private const int BATCH_SIZE = 100;\n private const int MAX_BATCHES = 1000;\n\n private const array SUPPORTED_OBJECT_TYPES = [\n self::OBJECT_TYPE_DEAL,\n self::OBJECT_TYPE_CONTACT,\n self::OBJECT_TYPE_COMPANY,\n ];\n\n public function __construct(\n private BatchSyncRedisService $redisService\n ) {\n }\n\n /**\n * Process batches for a specific object type.\n *\n * Drains the Redis set for the given object type and config, dispatching Import*Batch\n * jobs in batches of 100 until the set is empty or MAX_BATCHES is reached.\n *\n * @param string $objectType One of: 'deal', 'contact', 'company'\n * @param int $configId CRM configuration ID\n *\n * @return int Total number of IDs dispatched to jobs\n */\n public function processBatchesForObjectType(string $objectType, int $configId): int\n {\n if (! in_array($objectType, self::SUPPORTED_OBJECT_TYPES, true)) {\n Log::warning('[WebhookSyncBatchProcessor] Unsupported object type', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n ]);\n\n return 0;\n }\n\n $processedCount = 0;\n $batchNumber = 0;\n\n while ($batchNumber < self::MAX_BATCHES) {\n try {\n $crmProviderIds = $this->redisService->getCollectedObjects(\n $objectType,\n $configId,\n 'all',\n self::BATCH_SIZE\n );\n } catch (\\Throwable $e) {\n Log::error('[WebhookSyncBatchProcessor] Failed to fetch IDs from Redis', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'batch_number' => $batchNumber,\n 'error' => $e->getMessage(),\n ]);\n\n break;\n }\n\n if (empty($crmProviderIds)) {\n break;\n }\n\n try {\n $this->dispatchBatchJob($objectType, $configId, $crmProviderIds);\n\n $processedCount += \\count($crmProviderIds);\n $batchNumber++;\n } catch (\\Throwable $e) {\n Log::error('[WebhookSyncBatchProcessor] Batch dispatch failed, stopping', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'batch_number' => $batchNumber,\n 'batch_size' => \\count($crmProviderIds),\n 'error' => $e->getMessage(),\n ]);\n\n $this->requeueFailedIds($objectType, $configId, $crmProviderIds);\n\n break;\n }\n }\n\n if ($batchNumber >= self::MAX_BATCHES) {\n Log::warning('[WebhookSyncBatchProcessor] Maximum batch limit reached', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'max_batches' => self::MAX_BATCHES,\n 'total_dispatched' => $processedCount,\n ]);\n }\n\n if ($processedCount > 0) {\n Log::info('[WebhookSyncBatchProcessor] Batch processing completed', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'total_dispatched' => $processedCount,\n 'batches_dispatched' => $batchNumber,\n ]);\n }\n\n return $processedCount;\n }\n\n private function dispatchBatchJob(string $objectType, int $configId, array $crmProviderIds): void\n {\n match ($objectType) {\n self::OBJECT_TYPE_DEAL => ImportOpportunityBatch::dispatch($configId, $crmProviderIds),\n self::OBJECT_TYPE_CONTACT => ImportContactBatch::dispatch($configId, $crmProviderIds),\n self::OBJECT_TYPE_COMPANY => ImportAccountBatch::dispatch($configId, $crmProviderIds),\n };\n }\n\n private function requeueFailedIds(string $objectType, int $configId, array $crmProviderIds): void\n {\n $succeeded = 0;\n $permanentlyFailed = [];\n\n foreach ($crmProviderIds as $crmProviderId) {\n try {\n $this->redisService->collectObjectIdToBatch(\n $objectType,\n $crmProviderId,\n $configId,\n 'all'\n );\n $succeeded++;\n } catch (\\Throwable $idError) {\n $permanentlyFailed[] = $crmProviderId;\n Log::warning('[WebhookSyncBatchProcessor] Failed to requeue ID', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'crm_provider_id' => $crmProviderId,\n 'error' => $idError->getMessage(),\n ]);\n }\n }\n\n Log::info('[WebhookSyncBatchProcessor] Requeue completed', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'succeeded' => $succeeded,\n 'failed' => \\count($permanentlyFailed),\n 'total' => \\count($crmProviderIds),\n ]);\n\n if (! empty($permanentlyFailed)) {\n Log::error('[WebhookSyncBatchProcessor] Permanently failed to requeue IDs', [\n 'object_type' => $objectType,\n 'config_id' => $configId,\n 'failed_ids' => $permanentlyFailed,\n ]);\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-8010415956189026930
|
-8883631698440123185
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use Illuminate\Support\Facades\Log;
use Jiminny\Jobs\Crm\Hubspot\ImportAccountBatch;
use Jiminny\Jobs\Crm\Hubspot\ImportContactBatch;
use Jiminny\Jobs\Crm\Hubspot\ImportOpportunityBatch;
/**
* Processes batches of HubSpot objects collected from webhook events.
*
* This service drains Redis sets containing CRM IDs that were collected from webhook
* property change and association change events. It dispatches Import*Batch jobs to
* process the objects in batches of 100.
*
* Design Decisions:
*
* 1. Redis Service Injection:
* BatchSyncRedisService is injected via constructor for testability and to avoid
* tight coupling to Redis facade. This allows easy mocking in tests.
*
* 2. Multi-Object Type Support:
* processBatchesForObjectType() directly with their specific object type.
*
* 3. Batch Size and Limits:
* - BATCH_SIZE = 100: HubSpot batch API limit
* - MAX_BATCHES = 1000: Safety limit to prevent infinite loops (100K objects max per run)
*
* 4. Failure Handling:
* If job dispatch fails, IDs are requeued to Redis for the next processing cycle.
* Individual job failures are handled by the Import*Batch jobs themselves.
*/
class WebhookSyncBatchProcessor
{
public const string OBJECT_TYPE_DEAL = 'deal';
public const string OBJECT_TYPE_CONTACT = 'contact';
public const string OBJECT_TYPE_COMPANY = 'company';
private const int BATCH_SIZE = 100;
private const int MAX_BATCHES = 1000;
private const array SUPPORTED_OBJECT_TYPES = [
self::OBJECT_TYPE_DEAL,
self::OBJECT_TYPE_CONTACT,
self::OBJECT_TYPE_COMPANY,
];
public function __construct(
private BatchSyncRedisService $redisService
) {
}
/**
* Process batches for a specific object type.
*
* Drains the Redis set for the given object type and config, dispatching Import*Batch
* jobs in batches of 100 until the set is empty or MAX_BATCHES is reached.
*
* @param string $objectType One of: 'deal', 'contact', 'company'
* @param int $configId CRM configuration ID
*
* @return int Total number of IDs dispatched to jobs
*/
public function processBatchesForObjectType(string $objectType, int $configId): int
{
if (! in_array($objectType, self::SUPPORTED_OBJECT_TYPES, true)) {
Log::warning('[WebhookSyncBatchProcessor] Unsupported object type', [
'object_type' => $objectType,
'config_id' => $configId,
]);
return 0;
}
$processedCount = 0;
$batchNumber = 0;
while ($batchNumber < self::MAX_BATCHES) {
try {
$crmProviderIds = $this->redisService->getCollectedObjects(
$objectType,
$configId,
'all',
self::BATCH_SIZE
);
} catch (\Throwable $e) {
Log::error('[WebhookSyncBatchProcessor] Failed to fetch IDs from Redis', [
'object_type' => $objectType,
'config_id' => $configId,
'batch_number' => $batchNumber,
'error' => $e->getMessage(),
]);
break;
}
if (empty($crmProviderIds)) {
break;
}
try {
$this->dispatchBatchJob($objectType, $configId, $crmProviderIds);
$processedCount += \count($crmProviderIds);
$batchNumber++;
} catch (\Throwable $e) {
Log::error('[WebhookSyncBatchProcessor] Batch dispatch failed, stopping', [
'object_type' => $objectType,
'config_id' => $configId,
'batch_number' => $batchNumber,
'batch_size' => \count($crmProviderIds),
'error' => $e->getMessage(),
]);
$this->requeueFailedIds($objectType, $configId, $crmProviderIds);
break;
}
}
if ($batchNumber >= self::MAX_BATCHES) {
Log::warning('[WebhookSyncBatchProcessor] Maximum batch limit reached', [
'object_type' => $objectType,
'config_id' => $configId,
'max_batches' => self::MAX_BATCHES,
'total_dispatched' => $processedCount,
]);
}
if ($processedCount > 0) {
Log::info('[WebhookSyncBatchProcessor] Batch processing completed', [
'object_type' => $objectType,
'config_id' => $configId,
'total_dispatched' => $processedCount,
'batches_dispatched' => $batchNumber,
]);
}
return $processedCount;
}
private function dispatchBatchJob(string $objectType, int $configId, array $crmProviderIds): void
{
match ($objectType) {
self::OBJECT_TYPE_DEAL => ImportOpportunityBatch::dispatch($configId, $crmProviderIds),
self::OBJECT_TYPE_CONTACT => ImportContactBatch::dispatch($configId, $crmProviderIds),
self::OBJECT_TYPE_COMPANY => ImportAccountBatch::dispatch($configId, $crmProviderIds),
};
}
private function requeueFailedIds(string $objectType, int $configId, array $crmProviderIds): void
{
$succeeded = 0;
$permanentlyFailed = [];
foreach ($crmProviderIds as $crmProviderId) {
try {
$this->redisService->collectObjectIdToBatch(
$objectType,
$crmProviderId,
$configId,
'all'
);
$succeeded++;
} catch (\Throwable $idError) {
$permanentlyFailed[] = $crmProviderId;
Log::warning('[WebhookSyncBatchProcessor] Failed to requeue ID', [
'object_type' => $objectType,
'config_id' => $configId,
'crm_provider_id' => $crmProviderId,
'error' => $idError->getMessage(),
]);
}
}
Log::info('[WebhookSyncBatchProcessor] Requeue completed', [
'object_type' => $objectType,
'config_id' => $configId,
'succeeded' => $succeeded,
'failed' => \count($permanentlyFailed),
'total' => \count($crmProviderIds),
]);
if (! empty($permanentlyFailed)) {
Log::error('[WebhookSyncBatchProcessor] Permanently failed to requeue IDs', [
'object_type' => $objectType,
'config_id' => $configId,
'failed_ids' => $permanentlyFailed,
]);
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45155
|
1621
|
12
|
2026-05-14T14:22:58.920709+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768578920_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelpabl100% (C47•FV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTest* :Project v8• Thu 14 May 17:22:58Q© ReportController.php© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpPlaybackController.php=custom.log> D CrmObjects© UserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.php• DecorateActivity› D Dummy• DeleteCrmEntityTrait.phpDeleteAccountJob.php© ImportActivityTypes.php› HelpersT IntegrationAppTrait.php.env.staging.envDetachActivityObject.phpv HubspotClient.php18D AccountSyncStrate› DJ Actions1921D ContactSyncStrate!› O DTO› O Fields› D Journal© MatchActivityCrmData.phptrait WriteCrmTraitprivate const string ACTION_CREATE = 'create' :2 usages2 usages© HubspotPaginationService.php22private const string ACTION_UPDATE = 'update' ;23Metadataprivate const string ACTION_DELETE = 'delete';24v OpportunitySyncSti> [ Concerns25© HubspotLastMor26© HubspotLastMor27/***/* @inheritdoc© HubspotLastMor28 €public function saveActivity(Activity $activity): Activity© HubspotLastMor2930© HubspotLastMor31© HubspotSingleS:© HubspotSyncStr32switch ($activity->getCrmType)) {case Activity:: TYPE_CONFERENCE:Sactivity = $this->buildConferencePayload(Sactivity);© HubspotWebhoc33v D Pagination34© HubspotPaginati35© PaginationConfig36© PaginationState.37break;case Activity:: TYPE_SOFTPHONE:case Activity::TYPE_SOFTPHONE_INBOUND:Sactivity = $this->buildSoftphonePayload($activity);> D ProspectSearchStra38› [ Redis39break;v D ServiceTraits40€ OpportunitySync41© SyncCrmEntities42+ SyncFieldsTrait./43case Activity::TYPE_SMS_INBOUND:case Activity: : TYPE_SMS_OUTBOUND:Sactivity = $this->buildTextMessagePayload($activity);T WriteCrmTrait.pl44D Utils45break;D Webhook46@ RatchSvnrCnllertn'47Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)SendReportJob.php=laravel.logT WriteCrmTrait.php xAccountController.phpSF [jiminny@localhost]RematchActivityOnCrmObjectDetach.phpA HS_local [jiminny@localho:WHandleHubspotRateLimit.php4 console [QAI PROD] XA7 AV4 console [PROD]A console (EU]DGo jiminny08A1 43M4AY211m migrations oi212-213214215— 216m teams wherem crm_layouts !iM crm_layout_eiIM crm_fields Wi217218219220m features;m team_feature:m opportunitie:221222m teams;223224.id, CASEWHEN=225— 226227228229230,id FROM social,1 on V.id = sa.:1..n<->1: on t.Lid = 1052 and231232233 VIMaccounts wheiactivities234W Windsurf Teams28:21UTF-8Co 4 spaces...
|
NULL
|
-6457976466920580779
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelpabl100% (C47•FV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTest* :Project v8• Thu 14 May 17:22:58Q© ReportController.php© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpPlaybackController.php=custom.log> D CrmObjects© UserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.php• DecorateActivity› D Dummy• DeleteCrmEntityTrait.phpDeleteAccountJob.php© ImportActivityTypes.php› HelpersT IntegrationAppTrait.php.env.staging.envDetachActivityObject.phpv HubspotClient.php18D AccountSyncStrate› DJ Actions1921D ContactSyncStrate!› O DTO› O Fields› D Journal© MatchActivityCrmData.phptrait WriteCrmTraitprivate const string ACTION_CREATE = 'create' :2 usages2 usages© HubspotPaginationService.php22private const string ACTION_UPDATE = 'update' ;23Metadataprivate const string ACTION_DELETE = 'delete';24v OpportunitySyncSti> [ Concerns25© HubspotLastMor26© HubspotLastMor27/***/* @inheritdoc© HubspotLastMor28 €public function saveActivity(Activity $activity): Activity© HubspotLastMor2930© HubspotLastMor31© HubspotSingleS:© HubspotSyncStr32switch ($activity->getCrmType)) {case Activity:: TYPE_CONFERENCE:Sactivity = $this->buildConferencePayload(Sactivity);© HubspotWebhoc33v D Pagination34© HubspotPaginati35© PaginationConfig36© PaginationState.37break;case Activity:: TYPE_SOFTPHONE:case Activity::TYPE_SOFTPHONE_INBOUND:Sactivity = $this->buildSoftphonePayload($activity);> D ProspectSearchStra38› [ Redis39break;v D ServiceTraits40€ OpportunitySync41© SyncCrmEntities42+ SyncFieldsTrait./43case Activity::TYPE_SMS_INBOUND:case Activity: : TYPE_SMS_OUTBOUND:Sactivity = $this->buildTextMessagePayload($activity);T WriteCrmTrait.pl44D Utils45break;D Webhook46@ RatchSvnrCnllertn'47Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)SendReportJob.php=laravel.logT WriteCrmTrait.php xAccountController.phpSF [jiminny@localhost]RematchActivityOnCrmObjectDetach.phpA HS_local [jiminny@localho:WHandleHubspotRateLimit.php4 console [QAI PROD] XA7 AV4 console [PROD]A console (EU]DGo jiminny08A1 43M4AY211m migrations oi212-213214215— 216m teams wherem crm_layouts !iM crm_layout_eiIM crm_fields Wi217218219220m features;m team_feature:m opportunitie:221222m teams;223224.id, CASEWHEN=225— 226227228229230,id FROM social,1 on V.id = sa.:1..n<->1: on t.Lid = 1052 and231232233 VIMaccounts wheiactivities234W Windsurf Teams28:21UTF-8Co 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45156
|
1622
|
3
|
2026-05-14T14:22:58.942966+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768578942_m2.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
ActivityFilesLaterJiminny... ~@ jiminny-x-integrat ActivityFilesLaterJiminny... ~@ jiminny-x-integrati& platform-inner-team© Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity_lab# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the people of iimi..• Direct messages8. A...• A3 62P. Galya Dimitrova Mvasil VasilevA. Stefka Stoyanova%: Todor Stamatovf. Mario GeorgievP. Nikolay Ivanov2o James Graham "2. Stoyan Tanev. Steliyan Georgiev. Petko Kashinski*. Lukas Kovali...a: Apps® ToastS lira Gloud6 Huddle with Aneliya Angelova& R. Aneliya Angelova •• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някьде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova @ 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preview insackt[HubSpot] Optimise CRM rematctC OpenReady for QA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira+ SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle LIVE 4:06 PMAneliva Angelova is here toolAneliya Angelova 4:44 PM11512582Lukas Kovalik O 5:16 PMcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-Type: application/json'\--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIґ7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIAl Notes: OffLeave~ (Q SearchDate ModifiedYesterday at 13.34Yesterday at 13:34Yesterday at 13.33Yesterdav at 13:32resterday al i5.sYesterdav at 13:31Yesterday at 13:30Yesterday at 13.29Yesterday at 13:29resterday at 15.2oYesterday at 13:27Yesterday at 13:26Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:24Yesterday at 13:23Yesterday at 13:21Yesterday at 13:21Yesterday at 13.20Yesterdav at 13:19Yesterday at 13:19Yesterday at 13:18Yesterday at 13:18Yesterday at 13:17Yesterday at 13.16Yecterdav at 12:16Yesterday at 13:15Yesterday at 13.10Yesterdav at 13:14Yesterday at 13:14Yesterday at 13:13Yecterdav at 12:12Yesterday at 13.11Yesterdav at 13:11Yesterday at 13:10Yesterday at 13:09Yesterday at 13:09Yesterday at 13:08Yesterday at 13.08Yecterdav at 12:07Yesterday at 13:07Yesterdav at 13:0617K:MPEG-4 movie29 KBMPEG-4 movie6 KB12 Kb9 KBIMPEG-4 movieMPEG-4 movie7 KB8 KBMPEG-4 movie37 K:10 KBMPEG-4 movie7 KBMPEG-4 movie9 KBMPEG-4 movie8 KB72 KBMPEG-4 movie14 K:13 KBMPEG-4 movie9 KB18 KB|MPEG-4 movie12 KBMPEG-4 movie10 KB16 KBMPEG-4 movie6KEMPEG-4 movie6 KB MPEG-4 movie12 KB23 K:MPEG-4 movie8 KBMPEG-4 movie6 KBMPEG-4 movie11 KEMPEG-4 movie11 KBMPEG-4 movie20 KB34 K:MPEG-4 movie10 KBMPEG-4 movie7 KB5KBMPEG-4 movie11 KPMPEG-4 movie26 KB MPEG-4 movie111 KBMPEG-4 movie102 K:MPEG-4 movie88 KBMPEG-4 movie59 KB98 KBMPEG-4 movie07 KPMDEG-A movie66 KBMPEG-4 movie44 KBMPEG-4 movie03 K:IMPEG-A movid78 KB MPEG-4 movie50 KB58 KB27 KPMPEG-4 movieMPEG-4 movie7 KB12 KBMPEG-4 movie22 K:17 KBMPEG-4 movie19 KB32 K8MPEG-4 movieFavouritesE jiminny© Recents* ApplicationsiCloudiCloud Drive228 Sync folderQ DXP4800PLUS-B5FA@ Network|• CRMI• Orange• Red• Yellow• Green• Purple•) All lags..lohlDownloadsNameLoom.pkgAlfred copv.alfredoreterencesB KeychronAssist-1.0.2 (1).dmgA Keychron Assist-1.0.2.dmgmazanoke-images-ywJo.ziPhotos-3-001.zipD Transcript.pdf→mage U.loge1 Orioninstaller.dmaimage.jpg- image (2).1pc• ПО-22221726037035-004-001_ORGES.pdf• %D0%9F%D0%9E-22221726037035-004-001_archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterences05012026_0000000026574472_ SWIFT_OB70501260015890.pdf27022026_0000000026574472_SWIFT_OB72702260049200.pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001B reporti) xm=pdt.odipdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfB Rovaix Famly Treo gedbitwarden export 20251031122528.isonKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst_aid_notes_complete.docxrenortl2).esvconfig.ymlIteration run Search HS.postman_collection.json--report(1).csvm licence hettertouchtoalMariusHosting Config.json1ooks-891a6503-bbb7-4b2b-9c3.csv•Alfredmazanoke-images-YWJ6ана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик.jpg• Dhotac 2.001Q SearchKind00,4 MDinstdlle..dckage55.9 MBAlfred...ferences10,1 MBDisk Image10,1 MBDisk ImageIL MBLiP archive6,6 MBZIP archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDF Document140 KbZIP archive148 KВZIP archivel148 KBZIP archive122 KBXML document111 KBAlfred...ferences94 KBPDF Document92 KBPDr DocumentK:91 KB91 KB30 KB29 KBPDF DocumentXML documentPDF Document28 KBPDF Document28 KR28 KB27 KB14 KB11 KB6 KB6KBPDE DocumentDocumentJSONICSV DocumentZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBYAML document1 KBcSV Document928 byteshttlicence183 bytesZero butesJSONAlfred.. ferencesZero bytesFolde1,9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 9.63 GB availabld• Inu 14 May 1/.L2:00Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3713 Feb 2026 at 11:54Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nov 2025 at 17:596 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0418 Mar 2026 at 11:5510 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:3630 Jan 2026 at 12:3616 Oct 2025 aт 16:0123 Apr 2026 at 13:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
4675722753822127110
|
NULL
|
click
|
ocr
|
NULL
|
ActivityFilesLaterJiminny... ~@ jiminny-x-integrat ActivityFilesLaterJiminny... ~@ jiminny-x-integrati& platform-inner-team© Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity_lab# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the people of iimi..• Direct messages8. A...• A3 62P. Galya Dimitrova Mvasil VasilevA. Stefka Stoyanova%: Todor Stamatovf. Mario GeorgievP. Nikolay Ivanov2o James Graham "2. Stoyan Tanev. Steliyan Georgiev. Petko Kashinski*. Lukas Kovali...a: Apps® ToastS lira Gloud6 Huddle with Aneliya Angelova& R. Aneliya Angelova •• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някьде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova @ 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preview insackt[HubSpot] Optimise CRM rematctC OpenReady for QA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira+ SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle LIVE 4:06 PMAneliva Angelova is here toolAneliya Angelova 4:44 PM11512582Lukas Kovalik O 5:16 PMcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-Type: application/json'\--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIґ7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIAl Notes: OffLeave~ (Q SearchDate ModifiedYesterday at 13.34Yesterday at 13:34Yesterday at 13.33Yesterdav at 13:32resterday al i5.sYesterdav at 13:31Yesterday at 13:30Yesterday at 13.29Yesterday at 13:29resterday at 15.2oYesterday at 13:27Yesterday at 13:26Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:24Yesterday at 13:23Yesterday at 13:21Yesterday at 13:21Yesterday at 13.20Yesterdav at 13:19Yesterday at 13:19Yesterday at 13:18Yesterday at 13:18Yesterday at 13:17Yesterday at 13.16Yecterdav at 12:16Yesterday at 13:15Yesterday at 13.10Yesterdav at 13:14Yesterday at 13:14Yesterday at 13:13Yecterdav at 12:12Yesterday at 13.11Yesterdav at 13:11Yesterday at 13:10Yesterday at 13:09Yesterday at 13:09Yesterday at 13:08Yesterday at 13.08Yecterdav at 12:07Yesterday at 13:07Yesterdav at 13:0617K:MPEG-4 movie29 KBMPEG-4 movie6 KB12 Kb9 KBIMPEG-4 movieMPEG-4 movie7 KB8 KBMPEG-4 movie37 K:10 KBMPEG-4 movie7 KBMPEG-4 movie9 KBMPEG-4 movie8 KB72 KBMPEG-4 movie14 K:13 KBMPEG-4 movie9 KB18 KB|MPEG-4 movie12 KBMPEG-4 movie10 KB16 KBMPEG-4 movie6KEMPEG-4 movie6 KB MPEG-4 movie12 KB23 K:MPEG-4 movie8 KBMPEG-4 movie6 KBMPEG-4 movie11 KEMPEG-4 movie11 KBMPEG-4 movie20 KB34 K:MPEG-4 movie10 KBMPEG-4 movie7 KB5KBMPEG-4 movie11 KPMPEG-4 movie26 KB MPEG-4 movie111 KBMPEG-4 movie102 K:MPEG-4 movie88 KBMPEG-4 movie59 KB98 KBMPEG-4 movie07 KPMDEG-A movie66 KBMPEG-4 movie44 KBMPEG-4 movie03 K:IMPEG-A movid78 KB MPEG-4 movie50 KB58 KB27 KPMPEG-4 movieMPEG-4 movie7 KB12 KBMPEG-4 movie22 K:17 KBMPEG-4 movie19 KB32 K8MPEG-4 movieFavouritesE jiminny© Recents* ApplicationsiCloudiCloud Drive228 Sync folderQ DXP4800PLUS-B5FA@ Network|• CRMI• Orange• Red• Yellow• Green• Purple•) All lags..lohlDownloadsNameLoom.pkgAlfred copv.alfredoreterencesB KeychronAssist-1.0.2 (1).dmgA Keychron Assist-1.0.2.dmgmazanoke-images-ywJo.ziPhotos-3-001.zipD Transcript.pdf→mage U.loge1 Orioninstaller.dmaimage.jpg- image (2).1pc• ПО-22221726037035-004-001_ORGES.pdf• %D0%9F%D0%9E-22221726037035-004-001_archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterences05012026_0000000026574472_ SWIFT_OB70501260015890.pdf27022026_0000000026574472_SWIFT_OB72702260049200.pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001B reporti) xm=pdt.odipdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfB Rovaix Famly Treo gedbitwarden export 20251031122528.isonKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst_aid_notes_complete.docxrenortl2).esvconfig.ymlIteration run Search HS.postman_collection.json--report(1).csvm licence hettertouchtoalMariusHosting Config.json1ooks-891a6503-bbb7-4b2b-9c3.csv•Alfredmazanoke-images-YWJ6ана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик.jpg• Dhotac 2.001Q SearchKind00,4 MDinstdlle..dckage55.9 MBAlfred...ferences10,1 MBDisk Image10,1 MBDisk ImageIL MBLiP archive6,6 MBZIP archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDF Document140 KbZIP archive148 KВZIP archivel148 KBZIP archive122 KBXML document111 KBAlfred...ferences94 KBPDF Document92 KBPDr DocumentK:91 KB91 KB30 KB29 KBPDF DocumentXML documentPDF Document28 KBPDF Document28 KR28 KB27 KB14 KB11 KB6 KB6KBPDE DocumentDocumentJSONICSV DocumentZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBYAML document1 KBcSV Document928 byteshttlicence183 bytesZero butesJSONAlfred.. ferencesZero bytesFolde1,9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 9.63 GB availabld• Inu 14 May 1/.L2:00Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3713 Feb 2026 at 11:54Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nov 2025 at 17:596 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0418 Mar 2026 at 11:5510 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:3630 Jan 2026 at 12:3616 Oct 2025 aт 16:0123 Apr 2026 at 13:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
45152
|
NULL
|
NULL
|
NULL
|
|
45157
|
1621
|
13
|
2026-05-14T14:23:05.379144+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768585379_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelpabl100% C•FV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTest8• Thu 14 May 17:23:05* :QProject v© ReportController.php© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpPlaybackController.php:=custom.log> D CrmObjectsUserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpO DecorateActivity› D Dummy• DeleteCrmEntityTrait.phpDeleteAccountJob.php© ImportActivityTypes.phpT WriteCrmTrait.php xAccountController.php› HelpersT IntegrationAppTrait.php.env.staging.envDetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpv Hubspot© Client.php18D AccountSyncStrate› D ActionsD ContactSyncStrate!> DDTO› O Fields› D Journal© MatchActivityCrmData.phptrait WriteCrmTrait* Othrows HubspotException* @throws BadRequest*/1 usageHubspotPaginationService.phpHandleHubspotRateLimit.php19525354A8M6AYMetadata55private function puildConferencePayload(Activity $activity): Activity56v OpportunitySyncSti> [ Concerns57$decorator = $this->getDecorateActivity®:© HubspotLastMor58© HubspotLastMor59© HubspotLastMor68© HubspotLastMor6162© HubspotLastMor© HubspotSingleS:63$engagement = ['active' = true,'ownerid' => Sthis->profile->getCrmProviderId(),'timestamp' => $decorator->formatStartTime($activity),'type' => 'MEETING',© HubspotSyncStr641:© HubspotWebhoc6566v D Pagination© HubspotPaginati67if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType))) {$engagement['activityType'] = $activity->getActivityType()->getName();© PaginationConfig68© PaginationState.6970> D ProspectSearchStra> [ Redis71v D ServiceTraits72$metadata = ['meetingOutcome' => $decorator->convertMeetingOutcome($activity),'internalMeetingNotes' => $decorator-›generateDescription(Sactivity),€ OpportunitySync73© SyncCrmEntities74+ SyncFieldsTrait./75// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a (231T WriteCrmTrait.pl76if (! $activity->hasCrmProviderId)) {D Utils77if (Sactivity->hasActualStartTime() && $activity->hasActualEndTime()) {D Webhook78Smetadata[ 'startTime'] = $decorator->formatStartTime(Sactivity);D211212213214215216217218— 219220221222223224225=226_227228229230232- 233 /234@ RatchSvneCnllertn79Smetadata['endTime'] = Sdecoraton->formatEndTime(Sactivity);Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]Go jiminny0841A3 X4 A Vm migrations oim teams wherem crm_layouts !M crm_layout_eilIM crm_fields Wim features;m team_feature:m opportunitie:m teams;1.id, CASEWHENidFROMsocial,1 on u.id= sa.:1..n<->1: on t.L_id = 1052 andIMaccounts wheiactivities55:22UTF-8Co 4 spaces...
|
NULL
|
155710469272411052
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelpabl100% C•FV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTest8• Thu 14 May 17:23:05* :QProject v© ReportController.php© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpPlaybackController.php:=custom.log> D CrmObjectsUserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpO DecorateActivity› D Dummy• DeleteCrmEntityTrait.phpDeleteAccountJob.php© ImportActivityTypes.phpT WriteCrmTrait.php xAccountController.php› HelpersT IntegrationAppTrait.php.env.staging.envDetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpv Hubspot© Client.php18D AccountSyncStrate› D ActionsD ContactSyncStrate!> DDTO› O Fields› D Journal© MatchActivityCrmData.phptrait WriteCrmTrait* Othrows HubspotException* @throws BadRequest*/1 usageHubspotPaginationService.phpHandleHubspotRateLimit.php19525354A8M6AYMetadata55private function puildConferencePayload(Activity $activity): Activity56v OpportunitySyncSti> [ Concerns57$decorator = $this->getDecorateActivity®:© HubspotLastMor58© HubspotLastMor59© HubspotLastMor68© HubspotLastMor6162© HubspotLastMor© HubspotSingleS:63$engagement = ['active' = true,'ownerid' => Sthis->profile->getCrmProviderId(),'timestamp' => $decorator->formatStartTime($activity),'type' => 'MEETING',© HubspotSyncStr641:© HubspotWebhoc6566v D Pagination© HubspotPaginati67if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType))) {$engagement['activityType'] = $activity->getActivityType()->getName();© PaginationConfig68© PaginationState.6970> D ProspectSearchStra> [ Redis71v D ServiceTraits72$metadata = ['meetingOutcome' => $decorator->convertMeetingOutcome($activity),'internalMeetingNotes' => $decorator-›generateDescription(Sactivity),€ OpportunitySync73© SyncCrmEntities74+ SyncFieldsTrait./75// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a (231T WriteCrmTrait.pl76if (! $activity->hasCrmProviderId)) {D Utils77if (Sactivity->hasActualStartTime() && $activity->hasActualEndTime()) {D Webhook78Smetadata[ 'startTime'] = $decorator->formatStartTime(Sactivity);D211212213214215216217218— 219220221222223224225=226_227228229230232- 233 /234@ RatchSvneCnllertn79Smetadata['endTime'] = Sdecoraton->formatEndTime(Sactivity);Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]Go jiminny0841A3 X4 A Vm migrations oim teams wherem crm_layouts !M crm_layout_eilIM crm_fields Wim features;m team_feature:m opportunitie:m teams;1.id, CASEWHENidFROMsocial,1 on u.id= sa.:1..n<->1: on t.L_id = 1052 andIMaccounts wheiactivities55:22UTF-8Co 4 spaces...
|
45155
|
NULL
|
NULL
|
NULL
|
|
45158
|
1621
|
14
|
2026-05-14T14:23:07.298201+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768587298_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.054166667,"top":0.027777778,"width":0.08055556,"height":0.035555556},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.13472222,"top":0.027777778,"width":0.25555557,"height":0.035555556},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.6326389,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.6645833,"top":0.027777778,"width":0.15902779,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.82361114,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.8472222,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.87083334,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9291667,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9527778,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9763889,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"bounds":{"left":0.78055555,"top":0.24333334,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.80138886,"top":0.24333334,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.8215278,"top":0.24111111,"width":0.015277778,"height":0.025555555},"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.8368056,"top":0.24111111,"width":0.014583333,"height":0.025555555},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\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.85486114,"top":0.31,"width":0.018055556,"height":0.026666667},"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.87291664,"top":0.31,"width":0.018055556,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.9138889,"top":0.31,"width":0.059027776,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"bounds":{"left":0.8645833,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.8854167,"top":0.34444445,"width":0.015277778,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.9048611,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.92569447,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9458333,"top":0.3422222,"width":0.015277778,"height":0.025555555},"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.9611111,"top":0.3422222,"width":0.014583333,"height":0.025555555},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","depth":4,"on_screen":true,"value":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.025,"top":0.06666667,"width":0.050694443,"height":0.034444444},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3382584868951134281
|
6551498163460912845
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45159
|
1622
|
4
|
2026-05-14T14:23:05.452772+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768585452_m2.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.2962101,"top":1.0,"width":0.03856383,"height":-0.019952059},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.33477393,"top":1.0,"width":0.122340426,"height":-0.019952059},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.5731383,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.5884308,"top":1.0,"width":0.076130316,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.66456115,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-7759925082980464890
|
-7195274138718106135
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
ActivityFilesLaterJiminny... ~@ jiminny-x-integrati& platform-inner-team© Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity_lab# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the people of iimi..• Direct messages8. A...• A3 62P. Galya Dimitrova Mvasil VasilevA. Stefka Stoyanova%: Todor Stamatovf. Mario GeorgievP. Nikolay Ivanov2o James Graham "2. Stoyan Tanev. Steliyan Georgiev. Petko Kashinski*. Lukas Kovali...a: Apps® ToastS lira Gloud6 Huddle with Aneliya Angelova& R. Aneliya Angelova •• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някьде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova @ 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preview insackt[HubSpot] Optimise CRM rematctC OpenReady for QA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira+ SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle LIVE 4:06 PMAneliva Angelova is here toolAneliya Angelova 4:44 PM11512582Lukas Kovalik O 5:16 PMcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-Type: application/json'\--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIґ7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIAl Notes: OffLeave~ (Q SearchDate ModifiedYesterday at 13.34Yesterday at 13:34Yesterday at 13.33Yesterdav at 13:32resterday al i5.sYesterdav at 13:31Yesterday at 13:30Yesterday at 13.29Yesterday at 13:29resterday at 15.2oYesterday at 13:27Yesterday at 13:26Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:24Yesterday at 13:23Yesterday at 13:21Yesterday at 13:21Yesterday at 13.20Yesterdav at 13:19Yesterday at 13:19Yesterday at 13:18Yesterday at 13:18Yesterday at 13:17Yesterday at 13.16Yecterdav at 12:16Yesterday at 13:15resterday at 13-10Yesterdav at 13:14Yesterday at 13:14Yesterday at 13:13Yecterdav at 12:12Yesterday at 13.11Yesterdav at 13:11Yesterday at 13:10Yesterday at 13:09Yesterday at 13:09Yesterday at 13:08Yesterday at 13.08Yecterdav at 12:07Yesterday at 13:07Yesterdav at 13:0617K:MPEG-4 movie29 KBMPEG-4 movie6 KB12 Kb9 KBIMPEG-4 movieMPEG-4 movie7 KB8 KBMPEG-4 movie37 K:10 KBMPEG-4 movie7 KBMPEG-4 movie9 KBMPEG-4 movie8 KB72 KBMPEG-4 movie14 K:13 KBMPEG-4 movie9 KB18 KB|12 KBIMPEG-4 movieMPEG-4 movie10 KB16 KBMPEG-4 movie6KEMPEG-4 movie6 KB MPEG-4 movie12 KB23 K:MPEG-4 movie8 KBMPEG-4 movie6 KBMPEG-4 movie11 KEMPEG-4 movie11 KBMPEG-4 movie20 KB34 K:MPEG-4 movie10 KBMPEG-4 movie7 KB5KBMPEG-4 movie11 KPMPEG-4 movie26 KB MPEG-4 movie111 KBMPEG-4 movie102 K:MPEG-4 movie88 KBMPEG-4 movie59 KB98 KBMPEG-4 movie07 KPMDEG-A movie66 KBMPEG-4 movie44 KBMPEG-4 movie03 K:IMPEG-A movid78 KB MPEG-4 movie50 KB58 KB27 KPMPEG-4 movieMPEG-4 movie7 KB12 KBMPEG-4 movie22 K:17 KBMPEG-4 movie19 KB32 K8MPEG-4 movieFavouritesE jiminny© Recents* ApplicationsiCloudiCloud Drive228 Sync folderQ DXP4800PLUS-B5FA@ Network|• CRMI• Orange• Red• Yellow• Green• Purple•) All lags..lohlDownloadsNameLoom.pkgAlfred copv.alfredoreterencesB KeychronAssist-1.0.2 (1).dmgA Keychron Assist-1.0.2.dmgmazanoke-images-ywJo.ziPhotos-3-001.zipD Transcript.pdf→mage U.loge1 Orioninstaller.dmaimage.jpg- image (2).1pc• ПО-22221726037035-004-001_ORGES.pdf• %D0%9F%D0%9E-22221726037035-004-001_archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterences05012026_0000000026574472_ SWIFT_OB70501260015890.pdf27022026_0000000026574472_SWIFT_OB72702260049200.pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001B reporti) xm=pdt.odipdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfB Rovaix Famly Treo gedbitwarden export 20251031122528.isonKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst_aid_notes_complete.docxrenortl2).esvconfig.ymlIteration run Search HS.postman_collection.json--report(1).csvm licence hettertouchtoalMariusHosting Config.json1ooks-891a6503-bbb7-4b2b-9c3.csv•Alfredmazanoke-images-YWJ6ана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик.jpg• Dhotac 2.001Q SearchKind00,4 MDinstdlle..dckage55.9 MBAlfred...ferences10,1 MBDisk Image10,1 MBDisk ImageIL MBLiP archive6,6 MBZIP archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDF Document140 KbZIP archive148 KВZIP archivel148 KBZIP archive122 KBXML document111 KBAlfred...ferences94 KBPDF Document92 KBPDr DocumentK:91 KB91 KB30 KB29 KBPDF DocumentXML documentPDF Document28 KBPDF Document28 KR28 KB27 KB14 KB11 KB6 KB6KBPDE DocumentDocumentJSONICSV DocumentZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBYAML document1 KBcSV Document928 byteshttlicence183 bytesZero butesJSONAlfred.. ferencesZero bytesFolde1,9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 9.63 GB availabld• Inu 14 May 1/-23.02Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3713 Feb 2026 at 11:54Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nov 2025 at 17:596 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0418 Mar 2026 at 11:5510 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:3630 Jan 2026 at 12:3616 Oct 2025 aт 16:0123 Apr 2026 at 13:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45160
|
1622
|
5
|
2026-05-14T14:23:08.543772+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768588543_m2.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.2962101,"top":1.0,"width":0.03856383,"height":-0.019952059},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.33477393,"top":1.0,"width":0.122340426,"height":-0.019952059},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.5731383,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.5884308,"top":1.0,"width":0.076130316,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.66456115,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.67586434,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.6871675,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.7150931,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.72639626,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.73769945,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\n }\n}","depth":4,"bounds":{"left":0.39162233,"top":0.41340783,"width":0.31615692,"height":0.5865922},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","depth":4,"on_screen":true,"value":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.28224733,"top":1.0,"width":0.024268618,"height":-0.04788506},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3382584868951134281
|
6551498163460912845
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
45159
|
NULL
|
NULL
|
NULL
|
|
45161
|
1621
|
15
|
2026-05-14T14:23:10.574462+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768590574_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.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,"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
PhpStormFileEditViewNavig Project: faVsco.js, menu
PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelpabl100% C•FV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTest8• Thu 14 May 17:23:10* :QProject v© ReportController.php© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpPlaybackController.php=custom.log> D CrmObjectsUserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.phpSendReportJob.php• DecorateActivity› D Dummy• DeleteCrmEntityTrait.phpDeleteAccountJob.php© ImportActivityTypes.phpT WriteCrmTrait.php x© AccountController.php› HelpersT IntegrationAppTrait.php.env.staging.envDetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpv Hubspot© Client.php18D AccountSyncStrate› D ActionsD ContactSyncStrate!> DDTO› O Fields© MatchActivityCrmData.phptrait WriteCrmTrait* Othrows HubspotException* Othrows BadRequest*/1 usage© HubspotPaginationService.phpHandleHubspotRateLimit.php19525354A8M6AY› D Journal55private function buildConferencePayload(Activity Sactivity): ActivityMetadata56v OpportunitySyncSti57$decorator = $this->getDecorateActivity®:> [ Concerns58© HubspotLastMor59© HubspotLastMor60© HubspotLastMor61© HubspotLastMor62© HubspotLastMor63© HubspotSingleS:$engagement = ['active' => true,'ownerId' => $this->profile->getCrmProviderId(.'timestamp' => $decorator->formatStartTime(Sactivity),'type' => 'MEETING',64© HubspotSyncStr]:65© HubspotWebhoc66v D Pagination67if ($activity->hasActivityType() && $this-›matchesCrmType($activity->getActivityType())) {$engagement['activityType'] = $activity->getActivityType()->getName();D-—211212213214215216217218— 219220221Cascade & TI© HubspotPaginati68© PaginationConfig69224© PaginationState.22570> D ProspectSearchStra71> [ Redis72v D ServiceTraits$metadata = ['meetingOutcome' => $decorator->convertMeetingOutcome(Sactivity),'internalMeetingNotes' => $decorator-›generateDescription(Sactivity),=226_22773€ OpportunitySync1:22822974© SyncCrmEntities75230+ SyncFieldsTrait./// Only if they are creating a fresh engagement should we update the times, otherwise it will cause at76231if (! $activity->hasCrmProviderId)) {T WriteCrmTrait.pl77if (Sactivity->hasActualStartTime() && $activity->hasActualEndTime)232D Utils78— 233 VSmetadata['startTime'] = $decorator->formatStartTime($activity);D Webhook79@ RatchSvneCnllertn'Smetadata['endTime'] = $decorator->formatEndTime(Sactivity);234Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]Go jiminny0841A3 X4 A Vm migrations oim teams wherem crm_layouts !iM crm_layout_eiIM crm_fields Wim features;m team_feature:m opportunitie:Command 8811.id,CASEWHENidFROMsocial,I on u.id= sa.:1..n<->1: on t.L_id = 1052 andIMaccounts wheiactivitiesW Windsurf Teams66:39 (15 chars)UTF-8Co 4 spaces...
|
45158
|
NULL
|
NULL
|
NULL
|
|
45162
|
1622
|
6
|
2026-05-14T14:23:10.561487+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768590561_m2.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
ActivityFilesLaterJiminny... ~@ jiminny-x-integrat ActivityFilesLaterJiminny... ~@ jiminny-x-integrati& platform-inner-team© Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity_lab# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the people of iimi..• Direct messages8. A...• A3 62P. Galya Dimitrova Mvasil VasilevA. Stefka Stoyanova%: Todor Stamatovf. Mario GeorgievP. Nikolay Ivanov2o James Graham "2. Stoyan Tanev. Steliyan Georgiev. Petko Kashinski*. Lukas Kovali...a: Apps® ToastS lira Gloud6 Huddle with Aneliya Angelova& R. Aneliya Angelova •• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някьде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova @ 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preview insackt[HubSpot] Optimise CRM rematctC OpenReady for QA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira+ SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle LIVE 4:06 PMAneliva Angelova is here toolAneliya Angelova 4:44 PM11512582Lukas Kovalik O 5:16 PMcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-Type: application/json'\--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIґ7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIAl Notes: OffLeave~ (Q SearchDate ModifiedYesterday at 13.34Yesterday at 13:34Yesterday at 13.33Yesterdav at 13:32resterday al i5.sYesterdav at 13:31Yesterday at 13:30Yesterday at 13.29Yesterday at 13:29resterday at 15.2oYesterday at 13:27Yesterday at 13:26Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:24Yesterday at 13:23Yesterday at 13:21Yesterday at 13:21Yesterday at 13.20Yesterdav at 13:19Yesterday at 13:19Yesterday at 13:18Yesterday at 13:18Yesterday at 13:17Yesterday at 13.16Yecterdav at 12:16Yesterday at 13:15Yesterday at 13.10Yesterdav at 13:14Yesterday at 13:14Yesterday at 13:13Yecterdav at 12:12Yesterday at 13.11Yesterdav at 13:11Yesterday at 13:10Yesterday at 13:09Yesterday at 13:09Yesterday at 13:08Yesterday at 13.08Yecterdav at 12:07Yesterday at 13:07Yesterdav at 13:0617K:MPEG-4 movie29 KBMPEG-4 movie6 KB12 Kb9 KBIMPEG-4 movieMPEG-4 movie7 KB8 KBMPEG-4 movie37 K:10 KBMPEG-4 movie7 KBMPEG-4 movie9 KBMPEG-4 movie8 KB72 KBMPEG-4 movie14 K:13 KBMPEG-4 movie9 KB18 KB|MPEG-4 movie12 KBMPEG-4 movie10 KB16 KBMPEG-4 movie6KEMPEG-4 movie6 KB MPEG-4 movie12 KB23 K:MPEG-4 movie8 KBMPEG-4 movie6 KBMPEG-4 movie11 KEMPEG-4 movie11 KBMPEG-4 movie20 KB34 K:MPEG-4 movie10 KBMPEG-4 movie7 KB5KBMPEG-4 movie11 KPMPEG-4 movie26 KB MPEG-4 movie111 KBMPEG-4 movie102 K:MPEG-4 movie88 KBMPEG-4 movie59 KB98 KBMPEG-4 movie07 KPMDEG-A movie66 KBMPEG-4 movie44 KBMPEG-4 movie03 K:IMPEG-A movid78 KB MPEG-4 movie50 KB58 KB27 KPMPEG-4 movieMPEG-4 movie7 KB12 KBMPEG-4 movie22 K:17 KBMPEG-4 movie19 KB32 K8MPEG-4 movieFavouritesE jiminny© Recents* ApplicationsiCloudiCloud Drive228 Sync folderQ DXP4800PLUS-B5FA@ Network|• CRMI• Orange• Red• Yellow• Green• Purple•) All lags..lohlDownloadsNameLoom.pkgAlfred copv.alfredoreterencesB KeychronAssist-1.0.2 (1).dmgA Keychron Assist-1.0.2.dmgmazanoke-images-ywJo.ziPhotos-3-001.zipD Transcript.pdf→mage U.loge1 Orioninstaller.dmaimage.jpg- image (2).1pc• ПО-22221726037035-004-001_ORGES.pdf• %D0%9F%D0%9E-22221726037035-004-001_archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterences05012026_0000000026574472_ SWIFT_OB70501260015890.pdf27022026_0000000026574472_SWIFT_OB72702260049200.pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001B reporti) xm=pdt.odipdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfB Rovaix Famly Treo gedbitwarden export 20251031122528.isonKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst_aid_notes_complete.docxrenortl2).esvconfig.ymlIteration run Search HS.postman_collection.json--report(1).csvm licence hettertouchtoalMariusHosting Config.json1ooks-891a6503-bbb7-4b2b-9c3.csv•Alfredmazanoke-images-YWJ6ана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик.jpg• Dhotac 2.001Q SearchKind00,4 MDinstdlle..dckage55.9 MBAlfred...ferences10,1 MBDisk Image10,1 MBDisk ImageIL MBLiP archive6,6 MBZIP archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDF Document140 KbZIP archive148 KВZIP archivel148 KBZIP archive122 KBXML document111 KBAlfred...ferences94 KBPDF Document92 KBPDr DocumentK:91 KB91 KB30 KB29 KBPDF DocumentXML documentPDF Document28 KBPDF Document28 KR28 KB27 KB14 KB11 KB6 KB6KBPDE DocumentDocumentJSONICSV DocumentZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBYAML document1 KBcSV Document928 byteshttlicence183 bytesZero butesJSONAlfred.. ferencesZero bytesFolde1,9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 9.63 GB availabld• Inu 14 Mаy 1/.23.10Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3713 Feb 2026 at 11:54Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nov 2025 at 17:596 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0418 Mar 2026 at 11:5510 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:3630 Jan 2026 at 12:3616 Oct 2025 aт 16:0123 Apr 2026 at 13:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
-5443580408892116924
|
NULL
|
click
|
ocr
|
NULL
|
ActivityFilesLaterJiminny... ~@ jiminny-x-integrat ActivityFilesLaterJiminny... ~@ jiminny-x-integrati& platform-inner-team© Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity_lab# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the people of iimi..• Direct messages8. A...• A3 62P. Galya Dimitrova Mvasil VasilevA. Stefka Stoyanova%: Todor Stamatovf. Mario GeorgievP. Nikolay Ivanov2o James Graham "2. Stoyan Tanev. Steliyan Georgiev. Petko Kashinski*. Lukas Kovali...a: Apps® ToastS lira Gloud6 Huddle with Aneliya Angelova& R. Aneliya Angelova •• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някьде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova @ 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preview insackt[HubSpot] Optimise CRM rematctC OpenReady for QA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira+ SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle LIVE 4:06 PMAneliva Angelova is here toolAneliya Angelova 4:44 PM11512582Lukas Kovalik O 5:16 PMcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-Type: application/json'\--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIґ7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIAl Notes: OffLeave~ (Q SearchDate ModifiedYesterday at 13.34Yesterday at 13:34Yesterday at 13.33Yesterdav at 13:32resterday al i5.sYesterdav at 13:31Yesterday at 13:30Yesterday at 13.29Yesterday at 13:29resterday at 15.2oYesterday at 13:27Yesterday at 13:26Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:24Yesterday at 13:23Yesterday at 13:21Yesterday at 13:21Yesterday at 13.20Yesterdav at 13:19Yesterday at 13:19Yesterday at 13:18Yesterday at 13:18Yesterday at 13:17Yesterday at 13.16Yecterdav at 12:16Yesterday at 13:15Yesterday at 13.10Yesterdav at 13:14Yesterday at 13:14Yesterday at 13:13Yecterdav at 12:12Yesterday at 13.11Yesterdav at 13:11Yesterday at 13:10Yesterday at 13:09Yesterday at 13:09Yesterday at 13:08Yesterday at 13.08Yecterdav at 12:07Yesterday at 13:07Yesterdav at 13:0617K:MPEG-4 movie29 KBMPEG-4 movie6 KB12 Kb9 KBIMPEG-4 movieMPEG-4 movie7 KB8 KBMPEG-4 movie37 K:10 KBMPEG-4 movie7 KBMPEG-4 movie9 KBMPEG-4 movie8 KB72 KBMPEG-4 movie14 K:13 KBMPEG-4 movie9 KB18 KB|MPEG-4 movie12 KBMPEG-4 movie10 KB16 KBMPEG-4 movie6KEMPEG-4 movie6 KB MPEG-4 movie12 KB23 K:MPEG-4 movie8 KBMPEG-4 movie6 KBMPEG-4 movie11 KEMPEG-4 movie11 KBMPEG-4 movie20 KB34 K:MPEG-4 movie10 KBMPEG-4 movie7 KB5KBMPEG-4 movie11 KPMPEG-4 movie26 KB MPEG-4 movie111 KBMPEG-4 movie102 K:MPEG-4 movie88 KBMPEG-4 movie59 KB98 KBMPEG-4 movie07 KPMDEG-A movie66 KBMPEG-4 movie44 KBMPEG-4 movie03 K:IMPEG-A movid78 KB MPEG-4 movie50 KB58 KB27 KPMPEG-4 movieMPEG-4 movie7 KB12 KBMPEG-4 movie22 K:17 KBMPEG-4 movie19 KB32 K8MPEG-4 movieFavouritesE jiminny© Recents* ApplicationsiCloudiCloud Drive228 Sync folderQ DXP4800PLUS-B5FA@ Network|• CRMI• Orange• Red• Yellow• Green• Purple•) All lags..lohlDownloadsNameLoom.pkgAlfred copv.alfredoreterencesB KeychronAssist-1.0.2 (1).dmgA Keychron Assist-1.0.2.dmgmazanoke-images-ywJo.ziPhotos-3-001.zipD Transcript.pdf→mage U.loge1 Orioninstaller.dmaimage.jpg- image (2).1pc• ПО-22221726037035-004-001_ORGES.pdf• %D0%9F%D0%9E-22221726037035-004-001_archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterences05012026_0000000026574472_ SWIFT_OB70501260015890.pdf27022026_0000000026574472_SWIFT_OB72702260049200.pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001B reporti) xm=pdt.odipdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfB Rovaix Famly Treo gedbitwarden export 20251031122528.isonKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst_aid_notes_complete.docxrenortl2).esvconfig.ymlIteration run Search HS.postman_collection.json--report(1).csvm licence hettertouchtoalMariusHosting Config.json1ooks-891a6503-bbb7-4b2b-9c3.csv•Alfredmazanoke-images-YWJ6ана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик.jpg• Dhotac 2.001Q SearchKind00,4 MDinstdlle..dckage55.9 MBAlfred...ferences10,1 MBDisk Image10,1 MBDisk ImageIL MBLiP archive6,6 MBZIP archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDF Document140 KbZIP archive148 KВZIP archivel148 KBZIP archive122 KBXML document111 KBAlfred...ferences94 KBPDF Document92 KBPDr DocumentK:91 KB91 KB30 KB29 KBPDF DocumentXML documentPDF Document28 KBPDF Document28 KR28 KB27 KB14 KB11 KB6 KB6KBPDE DocumentDocumentJSONICSV DocumentZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBYAML document1 KBcSV Document928 byteshttlicence183 bytesZero butesJSONAlfred.. ferencesZero bytesFolde1,9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 9.63 GB availabld• Inu 14 Mаy 1/.23.10Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3713 Feb 2026 at 11:54Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nov 2025 at 17:596 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0418 Mar 2026 at 11:5510 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:3630 Jan 2026 at 12:3616 Oct 2025 aт 16:0123 Apr 2026 at 13:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45163
|
1621
|
16
|
2026-05-14T14:23:13.247292+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768593247_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelpabl100% C•FV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTest8• Thu 14 May 17:23:12* :QProject v© ReportController.php© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpPlaybackController.php=custom.log> D CrmObjectsUserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.phpSendReportJob.php• DecorateActivity› D Dummy• DeleteCrmEntityTrait.phpDeleteAccountJob.php© ImportActivityTypes.phpT WriteCrmTrait.php x© AccountController.php› HelpersT IntegrationAppTrait.php.env.staging.envDetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpv Hubspot© Client.php18D AccountSyncStrate› D ActionsD ContactSyncStrate!> DDTO› O Fields› D Journal© MatchActivityCrmData.phptrait WriteCrmTrait* @throws HubspotException* @throws BadRequest*/1 usageHubspotPaginationService.phpHandleHubspotRateLimit.php19525354Metadata55private function buildConferencePayload(Activity Sactivity): Activity56v OpportunitySyncSti> [ Concerns57$decorator = Sthis->getDecorateActivityO;© HubspotLastMor58© HubspotLastMor59© HubspotLastMor60© HubspotLastMor6162© HubspotLastMor63© HubspotSingleS:$engagement = l'active'= true,'ownerid' => Sthis->profile->getCrmProviderId(),'timestamp' => $decorator->formatStartTime(Sactivity),'type' => 'MEETING',64© HubspotSyncStr]:© HubspotWebhoc6566v D Pagination© HubspotPaginati6768© PaginationConfig© PaginationState.6970> D ProspectSearchStra> [ Redis71if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType))) {$engagement['activityType'] = $activity->getAct< 1/2 >T ActivityPlaybookTraitpublic function matchesCrmType(PlaybookCategory Scategory: boolv D ServiceTraits72Smetadata = ['meetingOutcome' => $decorator->convertMeeting0'internalMeetingNotes' => $decorator->generateD€ OpportunitySync73];© SyncCrmEntities74Parameters: \PlaybookCategory$category+ SyncFieldsTrait./75// Only if they are creating a fresh engagementshoT WriteCrmTrait.pl76if (! $activity->hasCrmProviderId()) {Source:.../app/Services/Crm/Helpers/ActivityPlaybookTrait.phpD Utils77if (Sactivity->hasActualStartTime() && $activitD Webhook78Smetadata('startTime'] = $decorator->format@ RatchSvneCnllertn'79Smetadata["ondTime!]l= Sdeconaton->formatEndTime (Sactivity):Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] XA8M6AY4 console [PROD]A console (EU]Go jiminny0841A3 X4 A Vm migrations oim teams wherem crm_layouts !iM crm_layout_eiIM crm_fields Wim features;team_feature:m opportunitie:m teams;1.id, CASEWHENidFROMsocial.1 on u.id= sa.:1..n<->1: on t.L_id = 1052 andLUcauseaD-—211212213214215216217218— 219220221222223224225=226_227228229230231232- 233 V234IMaccounts wheiactivitiesW Windsurf Teams66:60UTF-8Co 4 spaces...
|
NULL
|
-525233212572782706
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelpabl100% C•FV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTest8• Thu 14 May 17:23:12* :QProject v© ReportController.php© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpPlaybackController.php=custom.log> D CrmObjectsUserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.phpSendReportJob.php• DecorateActivity› D Dummy• DeleteCrmEntityTrait.phpDeleteAccountJob.php© ImportActivityTypes.phpT WriteCrmTrait.php x© AccountController.php› HelpersT IntegrationAppTrait.php.env.staging.envDetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpv Hubspot© Client.php18D AccountSyncStrate› D ActionsD ContactSyncStrate!> DDTO› O Fields› D Journal© MatchActivityCrmData.phptrait WriteCrmTrait* @throws HubspotException* @throws BadRequest*/1 usageHubspotPaginationService.phpHandleHubspotRateLimit.php19525354Metadata55private function buildConferencePayload(Activity Sactivity): Activity56v OpportunitySyncSti> [ Concerns57$decorator = Sthis->getDecorateActivityO;© HubspotLastMor58© HubspotLastMor59© HubspotLastMor60© HubspotLastMor6162© HubspotLastMor63© HubspotSingleS:$engagement = l'active'= true,'ownerid' => Sthis->profile->getCrmProviderId(),'timestamp' => $decorator->formatStartTime(Sactivity),'type' => 'MEETING',64© HubspotSyncStr]:© HubspotWebhoc6566v D Pagination© HubspotPaginati6768© PaginationConfig© PaginationState.6970> D ProspectSearchStra> [ Redis71if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType))) {$engagement['activityType'] = $activity->getAct< 1/2 >T ActivityPlaybookTraitpublic function matchesCrmType(PlaybookCategory Scategory: boolv D ServiceTraits72Smetadata = ['meetingOutcome' => $decorator->convertMeeting0'internalMeetingNotes' => $decorator->generateD€ OpportunitySync73];© SyncCrmEntities74Parameters: \PlaybookCategory$category+ SyncFieldsTrait./75// Only if they are creating a fresh engagementshoT WriteCrmTrait.pl76if (! $activity->hasCrmProviderId()) {Source:.../app/Services/Crm/Helpers/ActivityPlaybookTrait.phpD Utils77if (Sactivity->hasActualStartTime() && $activitD Webhook78Smetadata('startTime'] = $decorator->format@ RatchSvneCnllertn'79Smetadata["ondTime!]l= Sdeconaton->formatEndTime (Sactivity):Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] XA8M6AY4 console [PROD]A console (EU]Go jiminny0841A3 X4 A Vm migrations oim teams wherem crm_layouts !iM crm_layout_eiIM crm_fields Wim features;team_feature:m opportunitie:m teams;1.id, CASEWHENidFROMsocial.1 on u.id= sa.:1..n<->1: on t.L_id = 1052 andLUcauseaD-—211212213214215216217218— 219220221222223224225=226_227228229230231232- 233 V234IMaccounts wheiactivitiesW Windsurf Teams66:60UTF-8Co 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45179
|
1621
|
25
|
2026-05-14T14:23:50.137655+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768630137_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelpabl100% C•FV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTest8• Thu 14 May 17:23:50* :QProject v© ReportController.php© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpPlaybackController.php:=custom.log€ ConnectionStateTraUserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.phpSendReportJob.php© CrmHelperReposito© FilterJoinedParticip• DeleteCrmEntityTrait.phpDeleteAccountJob.php© ImportActivityTypes.phpWriteCrmTrait.php X© ActivityPlaybookTrait.phpT OpportunitySyncab© CrmHelperRepository.php© AccountController.phpT IntegrationAppTrait.php.env.staging.envv HubspotDetachActivityObject.phpC RematchActivityOnCrmObjectDetach.php© MatchActivityCrmData.php© Client.php18D AccountSyncStrate> DJ ActionsD ContactSyncStrate!› O DTO› O Fields› D JournalMetadata© HubspotPaginationService.phptrait WriteCrmTrait* @throws HubspotException* @throws BadRequest*/1 usageHandleHubspotRateLimit.php19525354A8 ж6^v OpportunitySyncSti55private function buildConferencePayload(Activity Sactivity): Activity> [ Concerns56© HubspotLastMor57$decorator = $this->getDecorateActivity();© HubspotLastMor58© HubspotLastMor59© HubspotLastMor60© HubspotLastMor61© HubspotSingleS:62© HubspotSyncStr63$engagement = l'active' => true,'ownerId' = $this->profile->getCrmProviderId®,'timestamp' => $decorator->formatStartTime(Sactivity),'type' => 'MEETING',© HubspotWebhoc64]:v D Pagination65© HubspotPaginati66© PaginationConfig67if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType))) {$engagement['activityType'] = $activity->getActivityType()->getNameO:© PaginationState.68> D ProspectSearchStra69› [ Redis70v D ServiceTraits71€ OpportunitySync72$metadata = ['meetingOutcome' => $decorator->convertMeetingOutcome(Sactivity),'internalMeetingNotes' => $decorator-›generateDescription($activity),© SyncCrmEntities73+ SyncFieldsTrait./74T WriteCrmTrait.pl75// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a ‹232D Utils76if (! $activity->hasCrmProviderId)) {D Webhook77if (Sactivity->hasActualStartTime() && $activity->hasActualEndTime)) {D— 211212- =226227- 228229230231- 233 v234@ RatchSvneCnllertn78Smetadatal 'startTime'l = Sdecorator->formatStartTime(Sactivitv):Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]Go jiminny0841A3 X4 A Vm migrations oim teams wherem crm_layouts !iM crm_layout_eiIM crm_fields Wim features;m team_feature:m opportunitie:m teams;1.id, CASEWHENidFROMsocial,1 on u.id= sa.:1..n<->1: on t.L_id = 1052 andIMaccounts wheiactivities66:60UTF-8Co 4 spaces...
|
NULL
|
-6670788643496401873
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelpabl100% C•FV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTest8• Thu 14 May 17:23:50* :QProject v© ReportController.php© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpPlaybackController.php:=custom.log€ ConnectionStateTraUserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.phpSendReportJob.php© CrmHelperReposito© FilterJoinedParticip• DeleteCrmEntityTrait.phpDeleteAccountJob.php© ImportActivityTypes.phpWriteCrmTrait.php X© ActivityPlaybookTrait.phpT OpportunitySyncab© CrmHelperRepository.php© AccountController.phpT IntegrationAppTrait.php.env.staging.envv HubspotDetachActivityObject.phpC RematchActivityOnCrmObjectDetach.php© MatchActivityCrmData.php© Client.php18D AccountSyncStrate> DJ ActionsD ContactSyncStrate!› O DTO› O Fields› D JournalMetadata© HubspotPaginationService.phptrait WriteCrmTrait* @throws HubspotException* @throws BadRequest*/1 usageHandleHubspotRateLimit.php19525354A8 ж6^v OpportunitySyncSti55private function buildConferencePayload(Activity Sactivity): Activity> [ Concerns56© HubspotLastMor57$decorator = $this->getDecorateActivity();© HubspotLastMor58© HubspotLastMor59© HubspotLastMor60© HubspotLastMor61© HubspotSingleS:62© HubspotSyncStr63$engagement = l'active' => true,'ownerId' = $this->profile->getCrmProviderId®,'timestamp' => $decorator->formatStartTime(Sactivity),'type' => 'MEETING',© HubspotWebhoc64]:v D Pagination65© HubspotPaginati66© PaginationConfig67if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType))) {$engagement['activityType'] = $activity->getActivityType()->getNameO:© PaginationState.68> D ProspectSearchStra69› [ Redis70v D ServiceTraits71€ OpportunitySync72$metadata = ['meetingOutcome' => $decorator->convertMeetingOutcome(Sactivity),'internalMeetingNotes' => $decorator-›generateDescription($activity),© SyncCrmEntities73+ SyncFieldsTrait./74T WriteCrmTrait.pl75// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a ‹232D Utils76if (! $activity->hasCrmProviderId)) {D Webhook77if (Sactivity->hasActualStartTime() && $activity->hasActualEndTime)) {D— 211212- =226227- 228229230231- 233 v234@ RatchSvneCnllertn78Smetadatal 'startTime'l = Sdecorator->formatStartTime(Sactivitv):Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]Go jiminny0841A3 X4 A Vm migrations oim teams wherem crm_layouts !iM crm_layout_eiIM crm_fields Wim features;m team_feature:m opportunitie:m teams;1.id, CASEWHENidFROMsocial,1 on u.id= sa.:1..n<->1: on t.L_id = 1052 andIMaccounts wheiactivities66:60UTF-8Co 4 spaces...
|
45178
|
NULL
|
NULL
|
NULL
|
|
45180
|
1622
|
14
|
2026-05-14T14:23:50.173150+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768630173_m2.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.2962101,"top":1.0,"width":0.03856383,"height":-0.019952059},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.33477393,"top":1.0,"width":0.122340426,"height":-0.019952059},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.5731383,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.5884308,"top":1.0,"width":0.076130316,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.66456115,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.67586434,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.6871675,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.7150931,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button"}]...
|
-5784594327568142026
|
-7196678489907033719
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preview insackt[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pmv Q SearchDate ModifiedYesterdav at 13.34Yesterday at 13:33Yesterday at 13.32Yesterdav at 13:32Yesterday at 13:31Yesterdav at 13:30Yesterday at 13:30Yesterday at 13.29Yesterdav at 13:28resterday at 15.21Yesterdav at 13:27Yesterday at 13:26Yesterday at 13:26Yesterday at 13:23Yesterdav at 13:25Yesterday at 13.24Yesterdav at 13:23Yesterday at 13:22Testerady dl ls.2.Yesterday at 13:21Voctordau at 12:20lYesterday at 13.19Yesterdav at 13:19Yesterdav at 13:18Yesterday at 13:18Yesterday at 13:17Yesterday at 13:17Yesterday at 13:16Yesterday at 13.16Yecterdav at 12:15Yesterdav at 13:15Yesterday at 13-14Yesterdav at 13:14Yesterday at 13:13Yesterday at 13:12Yecterdav at 12:12Yesterday at 13.11Yesterdav at 13:10Yesterday at 13:10Yesterday at 13:09Yoctorday at 12:09Yesterdav at 13:08Yesterday at 13.0Yecterdav at 12:07Yesterday at 13:06Yesterdav at 13:0529 K:MPEG-4 movie6 KB12 KBMDEG-A movieMPEG-4 movieMPEG-4 movie7 KBI8 KB37 KB10 K:MPEG-4 movie7 KBMDEG-A movie8 KEMPEG-4 movie8 KBI72 KB14 KB13 KEMPEG-4 movieMPEG-4 movie9 KBMDECA mavie18 KB12 KB|MPEG-4 movie10 KRI16 KB6 KBMPEG-1 movidMPEG-4 movie6KEMPEG-4 movie12 KBMPEG-4 movie23 KB8 K:MPEG-4 movie6 KB6 KB11 KBMDSG.A movicMPEG-4 movie11 KE20 KBMPEG-4 movieMPEG-4 movie34 KB10 K:MPEG-4 movie7 Kp5 KB11 KBMDSG.A movidMPEG-4 movie26KPMDEG-A movie111 KB MPEG-4 movie102 KB88 KB59 KB98 KB97 KBMPEG-4 movieMDEC.A moviaMPEG-4 movieRAKRMDEG-A movie44 KB93 KBMPEG-4 movie78 K:MPEG-A movie50 KBMPEG-4 movie58 KB27 KB7 KPMPEG-4 movieMDEG.A movio12 KB32 KBMPEG-4 movie17 Kг19 KBMDEC A movid32 KB10K8MPEG-4 movieAl Notes: OffLeave• • CFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc tolde0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfP Kdf-a,k Ffmiv Tree.sebitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenortl2).esviconfig.vmlIteration run Search HS.postman_collection.json--report(1).csvm licence hettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 9,62 GB availabld• Inu 14 Mау 1/•23.44Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 4+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45181
|
1621
|
26
|
2026-05-14T14:23:52.276580+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768632276_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelpabl100% C•FV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTest8• Thu 14 May 17:23:52* :QProject v© ReportController.php© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpPlaybackController.php:=custom.log€ ConnectionStateTraUserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.phpSendReportJob.php© CrmHelperReposito© FilterJoinedParticip• DeleteCrmEntityTrait.phpDeleteAccountJob.php© ImportActivityTypes.phpWriteCrmTrait.php X© ActivityPlaybookTrait.phpT OpportunitySyncab© CrmHelperRepository.php© AccountController.phpT IntegrationAppTrait.php.env.staging.envv HubspotDetachActivityObject.phpC RematchActivityOnCrmObjectDetach.php© MatchActivityCrmData.php© Client.php18D AccountSyncStrate› DJ ActionsD ContactSyncStrate!› O DTO› O Fields› D JournalMetadata© HubspotPaginationService.phptrait WriteCrmTrait* @throws HubspotException* @throws BadRequest*/1 usageHandleHubspotRateLimit.php19525354A8 ж6^v OpportunitySyncSti55private function buildConferencePayload(Activity Sactivity): Activity> [ Concerns56© HubspotLastMor57$decorator = $this->getDecorateActivity();© HubspotLastMor58© HubspotLastMor59© HubspotLastMor60© HubspotLastMor61© HubspotSingleS:62© HubspotSyncStr63$engagement= ['active' => true,'ownerId' = $this->profile->getCrmProviderId®,'timestamp' => $decorator->formatStartTime(Sactivity),'type' => 'MEETING',© HubspotWebhoc64]:v D Pagination65© HubspotPaginati66© PaginationConfig67if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType))) {Sengagement['activityType'] = $activity->getActivityType()->getName();© PaginationState.68> D ProspectSearchStra69› [ Redis70v D ServiceTraits71€ OpportunitySync72$metadata = ['meetingOutcome' => $decorator->convertMeetingOutcome(Sactivity),'internalMeetingNotes' => $decorator-›generateDescription($activity),© SyncCrmEntities73+ SyncFieldsTrait./74T WriteCrmTrait.pl75// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a ‹232D Utils76if (! $activity->hasCrmProviderId)) {D Webhook77if (Sactivity->hasActualStartTime() && $activity->hasActualEndTime)) {D— 211212-213214215216217218219-220221222223224225=226227-228229230231— 233 v234@ RatchSvneCnllertn78Smetadatal 'startTime'l = Sdecorator->formatStartTime(Sactivitv):Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]Go jiminny0841A3 X4 A Vm migrations oim teams wherem crm_layouts !iM crm_layout_eiIM crm_fields Wim features;m team_feature:m opportunitie:m teams;1.id, CASEWHENidFROMsocial,1 on u.id= sa.:1..n<->1: on t.L_id = 1052 andIMaccounts wheiactivities67:13UTF-8Co 4 spaces...
|
NULL
|
2572463782370577634
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelpabl100% C•FV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTest8• Thu 14 May 17:23:52* :QProject v© ReportController.php© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpPlaybackController.php:=custom.log€ ConnectionStateTraUserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.phpSendReportJob.php© CrmHelperReposito© FilterJoinedParticip• DeleteCrmEntityTrait.phpDeleteAccountJob.php© ImportActivityTypes.phpWriteCrmTrait.php X© ActivityPlaybookTrait.phpT OpportunitySyncab© CrmHelperRepository.php© AccountController.phpT IntegrationAppTrait.php.env.staging.envv HubspotDetachActivityObject.phpC RematchActivityOnCrmObjectDetach.php© MatchActivityCrmData.php© Client.php18D AccountSyncStrate› DJ ActionsD ContactSyncStrate!› O DTO› O Fields› D JournalMetadata© HubspotPaginationService.phptrait WriteCrmTrait* @throws HubspotException* @throws BadRequest*/1 usageHandleHubspotRateLimit.php19525354A8 ж6^v OpportunitySyncSti55private function buildConferencePayload(Activity Sactivity): Activity> [ Concerns56© HubspotLastMor57$decorator = $this->getDecorateActivity();© HubspotLastMor58© HubspotLastMor59© HubspotLastMor60© HubspotLastMor61© HubspotSingleS:62© HubspotSyncStr63$engagement= ['active' => true,'ownerId' = $this->profile->getCrmProviderId®,'timestamp' => $decorator->formatStartTime(Sactivity),'type' => 'MEETING',© HubspotWebhoc64]:v D Pagination65© HubspotPaginati66© PaginationConfig67if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType))) {Sengagement['activityType'] = $activity->getActivityType()->getName();© PaginationState.68> D ProspectSearchStra69› [ Redis70v D ServiceTraits71€ OpportunitySync72$metadata = ['meetingOutcome' => $decorator->convertMeetingOutcome(Sactivity),'internalMeetingNotes' => $decorator-›generateDescription($activity),© SyncCrmEntities73+ SyncFieldsTrait./74T WriteCrmTrait.pl75// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a ‹232D Utils76if (! $activity->hasCrmProviderId)) {D Webhook77if (Sactivity->hasActualStartTime() && $activity->hasActualEndTime)) {D— 211212-213214215216217218219-220221222223224225=226227-228229230231— 233 v234@ RatchSvneCnllertn78Smetadatal 'startTime'l = Sdecorator->formatStartTime(Sactivitv):Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]Go jiminny0841A3 X4 A Vm migrations oim teams wherem crm_layouts !iM crm_layout_eiIM crm_fields Wim features;m team_feature:m opportunitie:m teams;1.id, CASEWHENidFROMsocial,1 on u.id= sa.:1..n<->1: on t.L_id = 1052 andIMaccounts wheiactivities67:13UTF-8Co 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45182
|
1622
|
15
|
2026-05-14T14:23:54.568655+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768634568_m2.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.2962101,"top":1.0,"width":0.03856383,"height":-0.019952059},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.33477393,"top":1.0,"width":0.122340426,"height":-0.019952059},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.5731383,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.5884308,"top":1.0,"width":0.076130316,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.66456115,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.67586434,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.6871675,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.7150931,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.72639626,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.73769945,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\n }\n}","depth":4,"bounds":{"left":0.39195478,"top":0.3735036,"width":0.31615692,"height":0.62649643},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"}]...
|
5722231885167079023
|
-2691170301924544595
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3...
|
45180
|
NULL
|
NULL
|
NULL
|
|
45183
|
1621
|
27
|
2026-05-14T14:23:54.902821+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768634902_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.054166667,"top":0.027777778,"width":0.08055556,"height":0.035555556},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.13472222,"top":0.027777778,"width":0.25555557,"height":0.035555556},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.6326389,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.6645833,"top":0.027777778,"width":0.15902779,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.82361114,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.8472222,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.87083334,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9291667,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9527778,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9763889,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"bounds":{"left":0.78055555,"top":0.2777778,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.80138886,"top":0.2777778,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.8215278,"top":0.27555555,"width":0.015277778,"height":0.025555555},"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.8368056,"top":0.27555555,"width":0.014583333,"height":0.025555555},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\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.85486114,"top":0.31,"width":0.018055556,"height":0.026666667},"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.87291664,"top":0.31,"width":0.018055556,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.9138889,"top":0.31,"width":0.059027776,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"bounds":{"left":0.8645833,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.8854167,"top":0.34444445,"width":0.015277778,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.9048611,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.92569447,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9458333,"top":0.3422222,"width":0.015277778,"height":0.025555555},"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.9611111,"top":0.3422222,"width":0.014583333,"height":0.025555555},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","depth":4,"on_screen":true,"value":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.025,"top":0.06666667,"width":0.050694443,"height":0.034444444},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3382584868951134281
|
6551498163460912845
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
45181
|
NULL
|
NULL
|
NULL
|
|
45184
|
1621
|
28
|
2026-05-14T14:24:05.366794+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768645366_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.054166667,"top":0.027777778,"width":0.08055556,"height":0.035555556},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.13472222,"top":0.027777778,"width":0.25555557,"height":0.035555556},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.6326389,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.6645833,"top":0.027777778,"width":0.15902779,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.82361114,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.8472222,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.87083334,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9291667,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9527778,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9763889,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"bounds":{"left":0.78055555,"top":0.2777778,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.80138886,"top":0.2777778,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.8215278,"top":0.27555555,"width":0.015277778,"height":0.025555555},"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.8368056,"top":0.27555555,"width":0.014583333,"height":0.025555555},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false}]...
|
-8990710920805402580
|
-2691170164485591123
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45185
|
1622
|
16
|
2026-05-14T14:24:05.350072+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768645350_m2.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.2962101,"top":1.0,"width":0.03856383,"height":-0.019952059},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.33477393,"top":1.0,"width":0.122340426,"height":-0.019952059},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.5731383,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.5884308,"top":1.0,"width":0.076130316,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.66456115,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.67586434,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.6871675,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.7150931,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.72639626,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.73769945,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\n }\n}","depth":4,"bounds":{"left":0.39261967,"top":0.14844373,"width":0.31615692,"height":0.8515563},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
60362959092589173
|
-2691170164485591123
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45186
|
1621
|
29
|
2026-05-14T14:24:07.503713+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768647503_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.054166667,"top":0.027777778,"width":0.08055556,"height":0.035555556},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.13472222,"top":0.027777778,"width":0.25555557,"height":0.035555556},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.6326389,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.6645833,"top":0.027777778,"width":0.15902779,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.82361114,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.8472222,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.87083334,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9291667,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9527778,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9763889,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"bounds":{"left":0.78055555,"top":0.2777778,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.80138886,"top":0.2777778,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.8215278,"top":0.27555555,"width":0.015277778,"height":0.025555555},"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.8368056,"top":0.27555555,"width":0.014583333,"height":0.025555555},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\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.85486114,"top":0.31,"width":0.018055556,"height":0.026666667},"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.87291664,"top":0.31,"width":0.018055556,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.9138889,"top":0.31,"width":0.059027776,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"bounds":{"left":0.8645833,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.8854167,"top":0.34444445,"width":0.015277778,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.9048611,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.92569447,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9458333,"top":0.3422222,"width":0.015277778,"height":0.025555555},"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.9611111,"top":0.3422222,"width":0.014583333,"height":0.025555555},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","depth":4,"on_screen":true,"value":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.025,"top":0.06666667,"width":0.050694443,"height":0.034444444},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3382584868951134281
|
6551498163460912845
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
45184
|
NULL
|
NULL
|
NULL
|
|
45187
|
1621
|
30
|
2026-05-14T14:24:10.514575+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768650514_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactor Ru PhpStormFileEditViewNavigateCodeLaravelRefactor RunToolsGitWindowHelplhl••FV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTestProject v100% (C478• Thu 14 May 17:24:10Q© ReportController.php© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpPlaybackController.php:=custom.log€ ConnectionStateTraUserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.phpSendReportJob.php© CrmHelperReposito© FilterJoinedParticip© DeleteCrmEntityTrait.php© DeleteAccountJob.php© ImportActivityTypes.phpT WriteCrmTrait.php x© ActivityPlaybookTrait.phpT OpportunitySyncab© CrmHelperRepository.php© AccountController.phpT IntegrationAppTrait.php.env.staging.envv HubspotDetachActivityObject.phpC RematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.php18D AccountSyncStrate› DJ Actions© HandleHubspotRateLimit.phpD ContactSyncStrate!› O DTO› O Fields› D JournalMetadatav OpportunitySyncSti© HubspotPaginationService.phptrait WriteCrmTraitprivate function buildConferencePayload(Activity $activity): Activity'active'= true,'ownerid' => Sthis-›profile-›getCrmProviderId(),'timestamp' => $decorator->formatStartTime(Sactivity),'type' => 'MEETING',> [ Concerns© HubspotLastMor© HubspotLastMor© HubspotLastMor© HubspotLastMor© HubspotLastMor© HubspotSingleS:© HubspotSyncStr© HubspotWebhocv D Pagination© HubspotPaginati© PaginationConfig© PaginationState.> D ProspectSearchStra> [ Redisv D ServiceTraits€ OpportunitySync© SyncCrmEntities+ SyncFieldsTrait./T WriteCrmTrait.plD UtilsD Webhook A8M6AY1;if ($activity->hasActivityType() && $this-›matchesCrmType($activity->getActivityType())) {$engagement['activityType'] = $activity->getActivityType()->getName();Smetadata = ['meetingOutcome' => Sdecorator->convertMeetingOutcome(Sactivity),'internalMeetingNotes' => $decorator->generateDescription(Sactivity),// Only if they are creating a fresh engagement should we update the times, otherwise it will causeaif (! Sactivity->hasCrmProviderIdO) ‹if (Sactivity->hasActualStartTime() && $activity->hasActualEndTime()) {Smetadata[ 'startTime'] = $decorator->formatStartTime(Sactivity);$metadata['endTime'] = $decorator->formatEndTime($activity):$metadata['title'] = $decorator->generateTitle($activity);}-// For now, this is just a "meetingOutcome"field for some customers.Smetadata = array_merge(Smetadata, $this->fetchCustomFieldData(Sactivity, Field::0BJECT_TASK));D— 211212-213214215216217218219-220221222223224225226=227-228229230231232— 233 V234@ RatchSvnrCnllertn'Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]Go jiminny0841A3 X4 A Vm migrations oim teams wherem crm_layouts !M crm_layout_eilIM crm_fields Wim features;m team_feature:m opportunitie:m teams;1.id, CASEWHENidFROMsocial,1 on u.id= sa.:1..n<->1: on t.L_id = 1052 andIMaccounts wheiactivities81:57UTF-8Co 4 spaces...
|
NULL
|
-4979536423535327115
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactor Ru PhpStormFileEditViewNavigateCodeLaravelRefactor RunToolsGitWindowHelplhl••FV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTestProject v100% (C478• Thu 14 May 17:24:10Q© ReportController.php© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpPlaybackController.php:=custom.log€ ConnectionStateTraUserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.phpSendReportJob.php© CrmHelperReposito© FilterJoinedParticip© DeleteCrmEntityTrait.php© DeleteAccountJob.php© ImportActivityTypes.phpT WriteCrmTrait.php x© ActivityPlaybookTrait.phpT OpportunitySyncab© CrmHelperRepository.php© AccountController.phpT IntegrationAppTrait.php.env.staging.envv HubspotDetachActivityObject.phpC RematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.php18D AccountSyncStrate› DJ Actions© HandleHubspotRateLimit.phpD ContactSyncStrate!› O DTO› O Fields› D JournalMetadatav OpportunitySyncSti© HubspotPaginationService.phptrait WriteCrmTraitprivate function buildConferencePayload(Activity $activity): Activity'active'= true,'ownerid' => Sthis-›profile-›getCrmProviderId(),'timestamp' => $decorator->formatStartTime(Sactivity),'type' => 'MEETING',> [ Concerns© HubspotLastMor© HubspotLastMor© HubspotLastMor© HubspotLastMor© HubspotLastMor© HubspotSingleS:© HubspotSyncStr© HubspotWebhocv D Pagination© HubspotPaginati© PaginationConfig© PaginationState.> D ProspectSearchStra> [ Redisv D ServiceTraits€ OpportunitySync© SyncCrmEntities+ SyncFieldsTrait./T WriteCrmTrait.plD UtilsD Webhook A8M6AY1;if ($activity->hasActivityType() && $this-›matchesCrmType($activity->getActivityType())) {$engagement['activityType'] = $activity->getActivityType()->getName();Smetadata = ['meetingOutcome' => Sdecorator->convertMeetingOutcome(Sactivity),'internalMeetingNotes' => $decorator->generateDescription(Sactivity),// Only if they are creating a fresh engagement should we update the times, otherwise it will causeaif (! Sactivity->hasCrmProviderIdO) ‹if (Sactivity->hasActualStartTime() && $activity->hasActualEndTime()) {Smetadata[ 'startTime'] = $decorator->formatStartTime(Sactivity);$metadata['endTime'] = $decorator->formatEndTime($activity):$metadata['title'] = $decorator->generateTitle($activity);}-// For now, this is just a "meetingOutcome"field for some customers.Smetadata = array_merge(Smetadata, $this->fetchCustomFieldData(Sactivity, Field::0BJECT_TASK));D— 211212-213214215216217218219-220221222223224225226=227-228229230231232— 233 V234@ RatchSvnrCnllertn'Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]Go jiminny0841A3 X4 A Vm migrations oim teams wherem crm_layouts !M crm_layout_eilIM crm_fields Wim features;m team_feature:m opportunitie:m teams;1.id, CASEWHENidFROMsocial,1 on u.id= sa.:1..n<->1: on t.L_id = 1052 andIMaccounts wheiactivities81:57UTF-8Co 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45188
|
1621
|
31
|
2026-05-14T14:24:14.065542+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768654065_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.054166667,"top":0.027777778,"width":0.08055556,"height":0.035555556},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.13472222,"top":0.027777778,"width":0.25555557,"height":0.035555556},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.6326389,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.6645833,"top":0.027777778,"width":0.15902779,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.82361114,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.8472222,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.87083334,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9291667,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9527778,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9763889,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"bounds":{"left":0.78055555,"top":0.2777778,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.80138886,"top":0.2777778,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.8215278,"top":0.27555555,"width":0.015277778,"height":0.025555555},"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.8368056,"top":0.27555555,"width":0.014583333,"height":0.025555555},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\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.85486114,"top":0.31,"width":0.018055556,"height":0.026666667},"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.87291664,"top":0.31,"width":0.018055556,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
2768697343976106221
|
-2691170164485591123
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…...
|
45187
|
NULL
|
NULL
|
NULL
|
|
45189
|
1622
|
17
|
2026-05-14T14:24:14.066994+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768654066_m2.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.2962101,"top":1.0,"width":0.03856383,"height":-0.019952059},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.33477393,"top":1.0,"width":0.122340426,"height":-0.019952059},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.5731383,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.5884308,"top":1.0,"width":0.076130316,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.66456115,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.67586434,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.6871675,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.7150931,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.72639626,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.73769945,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","depth":4,"on_screen":true,"value":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.28224733,"top":1.0,"width":0.024268618,"height":-0.04788506},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3382584868951134281
|
6551498163460912845
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
45185
|
NULL
|
NULL
|
NULL
|
|
45190
|
1621
|
32
|
2026-05-14T14:24:16.570406+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768656570_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhlFV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTest100% <D8• Thu 14 May 17:24:16* :QProject v© ReportController.php© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpPlaybackController.phpcustom.log€ ConnectionStateTraUserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.php© CrmHelperReposito© FilterJoinedParticip• DeleteCrmEntityTrait.phpDeleteAccountJob.php© ImportActivityTypes.phpT OpportunitySyncab© CrmHelperRepository.php© AccountController.phpT IntegrationAppTrait.php.env.stagingv HubspotDetachActivityObject.phpRematchActivityOnCrmObjectDetach.php© MatchActivityCrmData.php18D AccountSyncStrate› D ActionsHandleHubspotRateLimit.phpD ContactSyncStrate!› O DTO© HubspotPaginationService.phptrait WriteCrmTraitprivate function buildConferencePayLoad(AAllClassesFilesSymbols1955Q-sfser› O Fields› D JournalMetadatav OpportunitySyncSti86878889return $this->upsertActivity($activit› O Concerns2 usages© HubspotLastMor90private function fetchCustomFieldData(Act© HubspotLastMor106© HubspotLastMor107 E© HubspotLastMor108© HubspotLastMor© HubspotSingleS:109110© HubspotSyncStr/*** @throws HubspotException* @throws BadRequest*/3 usages© HubspotWebhoc111private function upsertActivity(Activityv D Pagination|158© HubspotPaginati159© PaginationConfig160© PaginationState.161> D ProspectSearchStra162› [ Redisv D ServiceTraits163€ OpportunitySync164© SyncCrmEntities165+ SyncFieldsTrait./166T WriteCrmTrait.pl167D Utils168/*** @throws HubspotException* @throws BadRequest*/1 usageprivate function updateNonTaskActivity(Activity Sactivity,array $engagement,array Smetadata,array $oldAssociations): void {...}D Webhook183@ RatchSvnrCnllertnWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)SendReportJob.php=laravel.logWriteCrmTrait.php X© ActivityPlaybookTrait.phpSF [jiminny@localhost].envA HS_local [jiminny@localho:WClient.php4 console [QAI PROD] XActionsTextProject Files vSubmitAudioFileService.php .../app/.../AssemblyAl/Services/SubmitAudioFileService.php© SubmitAudioFileServiceTest.php .../.../AssemblyAl/SubmitAudioFileServiceTest.phpGenerateSpeechFromSilenceService.php .../.../Service/GenerateSpeechFromSilenceService.phpC GenerateSpeechFromSilenceServiceTest.php .../.../Service/GenerateSpeechFromSilenceServiceTest© SubmitAudioFileForLanguageDetectionService.php .../SubmitAudioFileForLanguageDetectionService© SubmitAudioFileForLanguageDetectionServiceTest.php .../tests/Unit/Compon...tectionServiceTest.pl© SyncCrmFieldService.php .../app/Component/AiAutomation/SyncCrmFieldService.php© AiScorecardFilterServiceTest.php .../tests/.../AiCallScoring/Services/AiScorecardFilterServiceTest.ph© AiScorecardFilterService.php .../app/Component/AiCallScoring/Services/AiScorecardFilterService.phO1S 1_elWIre:ie:EN228— 229_230— 231232- 233 V234Open In Right SplitJouLal,on u.id= sa.:1..n<->1: on t.L_id = 1052 andIMaccounts wheiactivitiesW Windsurf Teams108:32UTF-8Co 4 spaces...
|
NULL
|
5432217443116260450
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhlFV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTest100% <D8• Thu 14 May 17:24:16* :QProject v© ReportController.php© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpPlaybackController.phpcustom.log€ ConnectionStateTraUserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.php© CrmHelperReposito© FilterJoinedParticip• DeleteCrmEntityTrait.phpDeleteAccountJob.php© ImportActivityTypes.phpT OpportunitySyncab© CrmHelperRepository.php© AccountController.phpT IntegrationAppTrait.php.env.stagingv HubspotDetachActivityObject.phpRematchActivityOnCrmObjectDetach.php© MatchActivityCrmData.php18D AccountSyncStrate› D ActionsHandleHubspotRateLimit.phpD ContactSyncStrate!› O DTO© HubspotPaginationService.phptrait WriteCrmTraitprivate function buildConferencePayLoad(AAllClassesFilesSymbols1955Q-sfser› O Fields› D JournalMetadatav OpportunitySyncSti86878889return $this->upsertActivity($activit› O Concerns2 usages© HubspotLastMor90private function fetchCustomFieldData(Act© HubspotLastMor106© HubspotLastMor107 E© HubspotLastMor108© HubspotLastMor© HubspotSingleS:109110© HubspotSyncStr/*** @throws HubspotException* @throws BadRequest*/3 usages© HubspotWebhoc111private function upsertActivity(Activityv D Pagination|158© HubspotPaginati159© PaginationConfig160© PaginationState.161> D ProspectSearchStra162› [ Redisv D ServiceTraits163€ OpportunitySync164© SyncCrmEntities165+ SyncFieldsTrait./166T WriteCrmTrait.pl167D Utils168/*** @throws HubspotException* @throws BadRequest*/1 usageprivate function updateNonTaskActivity(Activity Sactivity,array $engagement,array Smetadata,array $oldAssociations): void {...}D Webhook183@ RatchSvnrCnllertnWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)SendReportJob.php=laravel.logWriteCrmTrait.php X© ActivityPlaybookTrait.phpSF [jiminny@localhost].envA HS_local [jiminny@localho:WClient.php4 console [QAI PROD] XActionsTextProject Files vSubmitAudioFileService.php .../app/.../AssemblyAl/Services/SubmitAudioFileService.php© SubmitAudioFileServiceTest.php .../.../AssemblyAl/SubmitAudioFileServiceTest.phpGenerateSpeechFromSilenceService.php .../.../Service/GenerateSpeechFromSilenceService.phpC GenerateSpeechFromSilenceServiceTest.php .../.../Service/GenerateSpeechFromSilenceServiceTest© SubmitAudioFileForLanguageDetectionService.php .../SubmitAudioFileForLanguageDetectionService© SubmitAudioFileForLanguageDetectionServiceTest.php .../tests/Unit/Compon...tectionServiceTest.pl© SyncCrmFieldService.php .../app/Component/AiAutomation/SyncCrmFieldService.php© AiScorecardFilterServiceTest.php .../tests/.../AiCallScoring/Services/AiScorecardFilterServiceTest.ph© AiScorecardFilterService.php .../app/Component/AiCallScoring/Services/AiScorecardFilterService.phO1S 1_elWIre:ie:EN228— 229_230— 231232- 233 V234Open In Right SplitJouLal,on u.id= sa.:1..n<->1: on t.L_id = 1052 andIMaccounts wheiactivitiesW Windsurf Teams108:32UTF-8Co 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45234
|
1621
|
61
|
2026-05-14T14:26:16.640026+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768776640_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.054166667,"top":0.027777778,"width":0.08055556,"height":0.035555556},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.13472222,"top":0.027777778,"width":0.25555557,"height":0.035555556},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.6326389,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.6645833,"top":0.027777778,"width":0.15902779,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.82361114,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.8472222,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.87083334,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9291667,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9527778,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9763889,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
2586234742233688171
|
-7051436856496256053
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelp•FV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit K vProjectv© ReportController.php© AutomatedReportGenerated.phpablHandleHubspotRateLimitTestv100% C8• Thu 14 May 17:26:16* :QTrackAutomatedReportGeneratedEvent.phpPlaybackController.php= custom.logT SyncCrmEntitiesTrait.phpUserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpT SyncFieldsTrait.php() WriteCrmTrait.php• DeleteCrmEntityTrait.phpDeleteAccountJob.php© ImportActivityTypes.phpT WriteCrmTrait.phpService.php> O Utils.T LogActivityTrait.php© PlainTextDecorateActivity.php© ActivityPlaybookTrait.phpCrmHelperRepository.php> D Webhook© AccountController.phpT IntegrationAppTrait.php.env.staging18.••E.env© DetachActivityObject.php© BatchSyncCollector.php© BatchSyncRedisService.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.phpHubspotPaginationService.php© Client.php© ClosedDealStagesService.pt19C DealFieldsService.php55© DecorateActivity.php© FieldDefinitions.php© FieldTypeConverter.php© HubspotClientinterface.php62© HubspotTokenManager.php© HandleHubspotRateLimit.phptrait WriteCrmTraitprivate function buildConferencePayload(Activity $activity): Activity'active' = true,'ownerid' = $this->profile->getCrmProviderIdO.'timestamp' => $decorator->formatStartTime($activity),'type' => 'MEETING',48 м6 ^606163© PayloadBuilder.php64© RemoteCrmObjectManipulat65ResponseNormalize.php66© Service.php67if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType))) {$engagement['activityType'] = $activity->getActivityType()->getNameO:© SyncFieldAction.php68}© SyncRelatedActivityManage69© WebhookSyncBatchProcess70v D IntegrationApp71> [J Accessors72$metadata = ['meetingOutcome'=> $decorator->convertMeetingOutcome($activity),'internalMeetingNotes' => $decorator->generateDescription(Sactivity),> D Api73];> D Config74D DTO75> O Filters76D Jobs770 ProspectSearchStrategy78> D ServiceTraits79// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a- 226if (! $activity->hasCrmProviderId()) {if (Sactivity->hasActualStartTime() && $activity->hasActualEndTime()) {Smetadata['startTime'] = $decorator->formatStartTime(Sactivity);Smetadata['endTime'] = $decorator->formatEndTime(Sactivity);© DataClient.php80}© DecorateActivity.php81$metadata['title'] = $decorator->generateTitle($activity);© LocalSearch.php82© LocalSearchinterface.php83D211-212— 213|214215216217218219111- 220|221222223224225= 227- 228229230231232233V234A DamntaCaarch nhn84// For now, this is just a "meetingOutcome" field for some customers.Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]Go jiminny0841A3 X4 A Vm migrations oim teams wherem crm_layouts !iM crm_layout_eiIM crm_fields Wim features;m team_feature:m opportunitie:m teams;1.id, CASEWHENid FROMsocial,1 on u.id= sa.:1..n<->1: on t.Lid = 1052 andIMaccounts wheiactivities108:32UTF-8Co 4 spaces...
|
45232
|
NULL
|
NULL
|
NULL
|
|
45235
|
1622
|
33
|
2026-05-14T14:26:19.051822+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768779051_m2.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.2962101,"top":1.0,"width":0.03856383,"height":-0.019952059},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.33477393,"top":1.0,"width":0.122340426,"height":-0.019952059},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.5731383,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.5884308,"top":1.0,"width":0.076130316,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.66456115,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.67586434,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-5840353787124240911
|
-7195274138713911831
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preuiew in sact[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIAl Notes: OffLeavev Q SearchDate ModifiedYesterday at 13.34Yesterday at 13:34Yesterday at 13.33Yesterdav at 13:32resterday al i5.sYesterdav at 13:31Yesterday at 13:30Yesterday at 13.29Yesterdav at 13:29resterday at 15.2oYesterday at 13:27Yesterday at 13:26Yesterday at 13:26Yesterdav at 13:25Yesterday at 13.24Yesterdav at 13:24Yesterday at 13:23Yesterday at 13:21Voctorday at 12:21Yesterday at 13.20Yesterdav at 13:19Yesterdav at 13:19Yesterday at 13:18Yesterday at 13:18Yesterday at 13:17Yesterday at 13.16Yecterdav at 12:16Yesterdav at 13:15Yesterday at 13.10Yesterdav at 13:14Yesterday at 13:14Yesterday at 13:13Yecterdav at 12:12Yesterday at 13.11Yesterdav at 13:11Yesterday at 13:10Yesterday at 13:09Yoctorday at 12:00Yesterdav at 13:08Yesterday at 13.08Yecterdav at 12:07Yesterday at 13:07Yesterdav at 13:0617K:MPEG-4 movie29 KB6 KB12 Kb9 KBI7 KB8 KB37 K:MDEG-A movieMPEG-4 movieMPEG-4 movieMPEG-4 movie10 KB7 KBMDEG-A movieMPEG-4 movie9 KBI8 KB72 KB14 K:MPEG-4 movieMPEG-4 movie13 KBMDECA mavile9 KB18 KB|MPEG-4 movie12 KBI10 KB16 KBMPEG-1 movidMPEG-4 movie6KEMPEG-4 movie6 KB MPEG-4 movie12 KB23 K:MPEG-4 movie8 KB6 KBMDSG.A movicMPEG-4 movie11 KE11 KBMPEG-4 movieMPEG-4 movie20 KB34 K:MPEG-4 movie10kр7 KB5KBMDEG.A movidMPEG-4 movie11 KPMDEG-A movie26 KB MPEG-4 movie111 KBMPEG-4 movie102 K:88 KB59 KB98 KBMPEG-4 movieMDEC.A moviaMPEG-4 movie07 KPMDEG-A movie66 KBMPEG-4 movie44 KBMPEG-4 movie03 K:IMPEG-A movid78 KB MPEG-4 movie50 KB58 KB27 KPMPEG-4 movieMDEG.A movio7 KB12 KBMPEG-4 movie22 K:17 KBMDEC A movid19 KB32 K8MPEG-4 movieFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc tolde0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfP Kdf-a,k Ffmiv Tree.sebitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenortl2).esviconfig.vmlIteration run Search HS.postman_collection.json--report(1).csvm licence hettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•Alfredv P1 mazanoko imaaoc. VWI6ана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.55 GB availabld• Inu 14 May 1/.20.10Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 4+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
45233
|
NULL
|
NULL
|
NULL
|
|
45236
|
1621
|
62
|
2026-05-14T14:26:21.175936+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768781175_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.054166667,"top":0.027777778,"width":0.08055556,"height":0.035555556},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.13472222,"top":0.027777778,"width":0.25555557,"height":0.035555556},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.6326389,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.6645833,"top":0.027777778,"width":0.15902779,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.82361114,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.8472222,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.87083334,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9291667,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9527778,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9763889,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3089888231946957708
|
-6979382010130101301
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelp•FV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit K vProjectv© ReportController.php© AutomatedReportGenerated.phpabl100% (C47HandleHubspotRateLimitTestv* :8• Thu 14 May 17:26:20QTrackAutomatedReportGeneratedEvent.phpPlaybackController.php= custom.logT SyncCrmEntitiesTrait.phpUserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpT SyncFieldsTrait.php() WriteCrmTrait.php• DeleteCrmEntityTrait.phpDeleteAccountJob.php© ImportActivityTypes.phpT WriteCrmTrait.phpService.php> O Utils.T LogActivityTrait.php© PlainTextDecorateActivity.phpActivityPlaybookTrait.phpCrmHelperRepository.php> D Webhook© AccountController.phpT IntegrationAppTrait.php.env.staging18E.env© DetachActivityObject.php© BatchSyncCollector.php© BatchSyncRedisService.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.phpHubspotPaginationService.php© Client.php© HandleHubspotRateLimit.php© ClosedDealStagesService.pt19trait WriteCrmTrait48 м6 ^DC DealFieldsService.php55private function buildConferencePayload(Activity $activity): Activity© DecorateActivity.php© FieldDefinitions.php60'active' = true,211© FieldTypeConverter.php61'ownerid' = $this->profile->getCrmProviderIdO.© HubspotClientinterface.php62'timestamp' => $decorator->formatStartTime($activity),—212— 213|© HubspotTokenManager.php63'type' => 'MEETING',214© PayloadBuilder.php64215© RemoteCrmObjectManipulat65216ResponseNormalize.php66if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType))) {217© Service.php67$engagement['activityType'] = $activity->getActivityType()->getNameO:218© SyncFieldAction.php68}219© SyncRelatedActivityManage69220© WebhookSyncBatchProcess70$metadata = [-221v D IntegrationApp71'meetingOutcome' => $decorator->convertMeetingOutcome($activity),222> [J Accessors72'internalMeetingNotes' => $decorator->generateDescription(Sactivity),223> Api|73];224> D Config74225D DTO75// Only if they are creating a fresh engagement should we update the times, otherwise it will causea226D Filters76if (! $activity->hasCrmProviderId()) {=227D Jobs77if (Sactivity->hasActualStartTime() && $activity->hasActualEndTime)) {— 2280 ProspectSearchStrategy78Smetadata['startTime'] = $decorator->formatStartTime(Sactivity);229> D ServiceTraits79Smetadata['endTime'] = $decorator->formatEndTime(Sactivity);230© DataClient.php80— 231© DecorateActivity.php81$metadata['title'] = $decorator-›generateTitle($activity);232© LocalSearch.php82233 V© LocalSearchinterface.php83234A DamntaCaarch nhn84// For now, this is just a "meetingOutcome" field for some customers.Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]Go jiminny0841A3 X4 A Vm migrations oim teams wherem crm_layouts !iM crm_layout_eiIM crm_fields Wim features;m team_feature:m opportunitie:m teams;1.id, CASEWHENidFROMsocial,1 on u.id= sa.:1..n<->1: on t.L_id = 1052 andIMaccounts wheiactivities66:29UTF-8Co 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45237
|
1621
|
63
|
2026-05-14T14:26:24.776201+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768784776_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelp•FV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit k vProjectv© ReportController.php© AutomatedReportGenerated.phplhlHandleHubspotRateLimitTest v100% (C478• Thu 14 May 17:26:24* :QTrackAutomatedReportGeneratedEvent.phpPlaybackController.php= custom.logT SyncCrmEntitiesTrait.phpUserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.php© SendReportJob.phpT SyncFieldsTrait.php() WriteCrmTrait.php• DeleteCrmEntityTrait.phpDeleteAccountJob.php© ImportActivityTypes.phpT WriteCrmTrait.phpService.php> O Utils.T LogActivityTrait.php© PlainTextDecorateActivity.php© ActivityPlaybookTrait.phpCrmHelperRepository.php> D Webhook© AccountController.phpT IntegrationAppTrait.php.env.staging18.••E.env© DetachActivityObject.php© BatchSyncCollector.php© BatchSyncRedisService.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.phpHubspotPaginationService.php© Client.php© ClosedDealStagesService.pt19C DealFieldsService.php© DecorateActivity.php© FieldDefinitions.php© HandleHubspotRateLimit.phptrait WriteCrmTraitprivate function buildConferencePayload(Activity $activity): Activity'timestamp'=>$decorator->formatStartTime(Sactivity),'type' => 'MEETING',48 м6 ^556263© FieldTypeConverter.php641;© HubspotClientinterface.php65© HubspotTokenManager.php66© PayloadBuilder.php67if ($activity->hasActivityType() && $this-›matchesCrmType($activity->getActivityType())){$engagemeht['activityType'] = $activity->getActivityType()->getName();© RemoteCrmObjectManipulatResponseNormalize.php6869© Service.php70© SyncFieldAction.php71© SyncRelatedActivityManage72$metadata = ['meetingOutcome' => $decorator->convertMeetingOutcome(Sactivity),'internalMeetingNotes' => $decorator->generateDescription(Sactivity),© WebhookSyncBatchProcess731;v D IntegrationApp@ ServiceTraits74> [J Accessors75> D Api> D Config7677D DTO78> O Filters79// Only if they are creating aif (! Sactivity->hasCrmProvideif (Sactivity->hasActualStSmetadata('startTime')$metadata[ 'endTime']$decorator: DecorateActivityimes, otherwise it will causeaSource: .../app/Services/Crm/Hubspot/Service Traits/WriteCrmTrait.php• :D Jobs800 ProspectSearchStrategy81$metadata['title'] = $decorator-›generateTitle($activity);> D ServiceTraits82© DataClient.php83© DecorateActivity.php84© LocalSearch.php85// For now, this is just a "meetingOutcome" field for some customers.Smetadata = array_merge(Smetadata, $this->fetchCustomFieldData(Sactivity, Field::0BJECT_TASK));© LocalSearchinterface.php86A DamntaCaarch nhnWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]D211—212— 213| = 227— 228229230231232_ 233 v234Go jiminny0841A3 X4 A Vm migrations oim teams wherem crm_layouts !iM crm_layout_eiIM crm_fields Wim features;m team_feature:m opportunitie:m teams;1.id, CASEWHENidFROMsocial,on u.id= sa.:1..n<->1: on t.L_id = 1052 andIMaccounts wheiactivities67:22UTF-8Co 4 spaces...
|
NULL
|
-6659808716661506122
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelp•FV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit k vProjectv© ReportController.php© AutomatedReportGenerated.phplhlHandleHubspotRateLimitTest v100% (C478• Thu 14 May 17:26:24* :QTrackAutomatedReportGeneratedEvent.phpPlaybackController.php= custom.logT SyncCrmEntitiesTrait.phpUserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.php© SendReportJob.phpT SyncFieldsTrait.php() WriteCrmTrait.php• DeleteCrmEntityTrait.phpDeleteAccountJob.php© ImportActivityTypes.phpT WriteCrmTrait.phpService.php> O Utils.T LogActivityTrait.php© PlainTextDecorateActivity.php© ActivityPlaybookTrait.phpCrmHelperRepository.php> D Webhook© AccountController.phpT IntegrationAppTrait.php.env.staging18.••E.env© DetachActivityObject.php© BatchSyncCollector.php© BatchSyncRedisService.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.phpHubspotPaginationService.php© Client.php© ClosedDealStagesService.pt19C DealFieldsService.php© DecorateActivity.php© FieldDefinitions.php© HandleHubspotRateLimit.phptrait WriteCrmTraitprivate function buildConferencePayload(Activity $activity): Activity'timestamp'=>$decorator->formatStartTime(Sactivity),'type' => 'MEETING',48 м6 ^556263© FieldTypeConverter.php641;© HubspotClientinterface.php65© HubspotTokenManager.php66© PayloadBuilder.php67if ($activity->hasActivityType() && $this-›matchesCrmType($activity->getActivityType())){$engagemeht['activityType'] = $activity->getActivityType()->getName();© RemoteCrmObjectManipulatResponseNormalize.php6869© Service.php70© SyncFieldAction.php71© SyncRelatedActivityManage72$metadata = ['meetingOutcome' => $decorator->convertMeetingOutcome(Sactivity),'internalMeetingNotes' => $decorator->generateDescription(Sactivity),© WebhookSyncBatchProcess731;v D IntegrationApp@ ServiceTraits74> [J Accessors75> D Api> D Config7677D DTO78> O Filters79// Only if they are creating aif (! Sactivity->hasCrmProvideif (Sactivity->hasActualStSmetadata('startTime')$metadata[ 'endTime']$decorator: DecorateActivityimes, otherwise it will causeaSource: .../app/Services/Crm/Hubspot/Service Traits/WriteCrmTrait.php• :D Jobs800 ProspectSearchStrategy81$metadata['title'] = $decorator-›generateTitle($activity);> D ServiceTraits82© DataClient.php83© DecorateActivity.php84© LocalSearch.php85// For now, this is just a "meetingOutcome" field for some customers.Smetadata = array_merge(Smetadata, $this->fetchCustomFieldData(Sactivity, Field::0BJECT_TASK));© LocalSearchinterface.php86A DamntaCaarch nhnWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]D211—212— 213| = 227— 228229230231232_ 233 v234Go jiminny0841A3 X4 A Vm migrations oim teams wherem crm_layouts !iM crm_layout_eiIM crm_fields Wim features;m team_feature:m opportunitie:m teams;1.id, CASEWHENidFROMsocial,on u.id= sa.:1..n<->1: on t.L_id = 1052 andIMaccounts wheiactivities67:22UTF-8Co 4 spaces...
|
45236
|
NULL
|
NULL
|
NULL
|
|
45238
|
1622
|
34
|
2026-05-14T14:26:24.751573+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768784751_m2.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Jiminny... ~ActivityFilesLater@ jiminny-x-integrat Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preuiew in sact[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIAl Notes: OffLeavev Q SearchDate ModifiedYesterday at 13.34Yesterday at 13:34Yesterday at 13.33Yesterdav at 13:32resterday al i5.sYesterdav at 13:31Yesterday at 13:30Yesterday at 13.29Yesterdav at 13:29resterday at 15.2oYesterday at 13:27Yesterday at 13:26Yesterday at 13:26Yesterdav at 13:25Yesterday at 13.24Yesterdav at 13:24Yesterday at 13:23Yesterday at 13:21Voctorday at 12:21Yesterday at 13.20Yesterdav at 13:19Yesterdav at 13:19Yesterday at 13:18Yesterday at 13:18Yesterday at 13:17Yesterday at 13.16Yecterdav at 12:16Yesterdav at 13:15Yesterday at 13.10Yesterdav at 13:14Yesterday at 13:14Yesterday at 13:13Yecterdav at 12:12Yesterday at 13.11Yesterdav at 13:11Yesterday at 13:10Yesterday at 13:09Yoctorday at 12:00Yesterdav at 13:08Yesterday at 13.08Yecterdav at 12:07Yesterday at 13:07Yesterdav at 13:0617K:MPEG-4 movie29 KB6 KB12 Kb9 KBI7 KB8 KB37 K:MDEG-A movieMPEG-4 movieMPEG-4 movieMPEG-4 movie10 KB7 KBMDEG-A movieMPEG-4 movie9 KBI8 KB72 KB14 K:MPEG-4 movieMPEG-4 movie13 KBMDECA mavile9 KB18 KB|MPEG-4 movie12 KBI10 KB16 KBMPEG-1 movidMPEG-4 movie6KEMPEG-4 movie6 KB MPEG-4 movie12 KB23 K:MPEG-4 movie8 KB6 KBMDSG.A movicMPEG-4 movie11 KE11 KBMPEG-4 movieMPEG-4 movie20 KB34 K:MPEG-4 movie10kр7 KB5KBMDEG.A movidMPEG-4 movie11 KPMDEG-A movie26 KB MPEG-4 movie111 KBMPEG-4 movie102 K:88 KB59 KB98 KBMPEG-4 movieMDEC.A moviaMPEG-4 movie07 KPMDEG-A movie66 KBMPEG-4 movie44 KBMPEG-4 movie03 K:IMPEG-A movid78 KB MPEG-4 movie50 KB58 KB27 KPMPEG-4 movieMDEG.A movio7 KB12 KBMPEG-4 movie22 K:17 KBMDEC A movid19 KB32 K8MPEG-4 movieFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc tolde0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfP Kdf-a,k Ffmiv Tree.sebitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenortl2).esviconfig.vmlIteration run Search HS.postman_collection.json--report(1).csvm licence hettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•Alfredv P1 mazanoko imaaoc. VWI6ана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.56 GB availabld• Inu 14 May 1/:20.24Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 4+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
5778817823299214474
|
NULL
|
click
|
ocr
|
NULL
|
Jiminny... ~ActivityFilesLater@ jiminny-x-integrat Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preuiew in sact[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIAl Notes: OffLeavev Q SearchDate ModifiedYesterday at 13.34Yesterday at 13:34Yesterday at 13.33Yesterdav at 13:32resterday al i5.sYesterdav at 13:31Yesterday at 13:30Yesterday at 13.29Yesterdav at 13:29resterday at 15.2oYesterday at 13:27Yesterday at 13:26Yesterday at 13:26Yesterdav at 13:25Yesterday at 13.24Yesterdav at 13:24Yesterday at 13:23Yesterday at 13:21Voctorday at 12:21Yesterday at 13.20Yesterdav at 13:19Yesterdav at 13:19Yesterday at 13:18Yesterday at 13:18Yesterday at 13:17Yesterday at 13.16Yecterdav at 12:16Yesterdav at 13:15Yesterday at 13.10Yesterdav at 13:14Yesterday at 13:14Yesterday at 13:13Yecterdav at 12:12Yesterday at 13.11Yesterdav at 13:11Yesterday at 13:10Yesterday at 13:09Yoctorday at 12:00Yesterdav at 13:08Yesterday at 13.08Yecterdav at 12:07Yesterday at 13:07Yesterdav at 13:0617K:MPEG-4 movie29 KB6 KB12 Kb9 KBI7 KB8 KB37 K:MDEG-A movieMPEG-4 movieMPEG-4 movieMPEG-4 movie10 KB7 KBMDEG-A movieMPEG-4 movie9 KBI8 KB72 KB14 K:MPEG-4 movieMPEG-4 movie13 KBMDECA mavile9 KB18 KB|MPEG-4 movie12 KBI10 KB16 KBMPEG-1 movidMPEG-4 movie6KEMPEG-4 movie6 KB MPEG-4 movie12 KB23 K:MPEG-4 movie8 KB6 KBMDSG.A movicMPEG-4 movie11 KE11 KBMPEG-4 movieMPEG-4 movie20 KB34 K:MPEG-4 movie10kр7 KB5KBMDEG.A movidMPEG-4 movie11 KPMDEG-A movie26 KB MPEG-4 movie111 KBMPEG-4 movie102 K:88 KB59 KB98 KBMPEG-4 movieMDEC.A moviaMPEG-4 movie07 KPMDEG-A movie66 KBMPEG-4 movie44 KBMPEG-4 movie03 K:IMPEG-A movid78 KB MPEG-4 movie50 KB58 KB27 KPMPEG-4 movieMDEG.A movio7 KB12 KBMPEG-4 movie22 K:17 KBMDEC A movid19 KB32 K8MPEG-4 movieFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc tolde0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfP Kdf-a,k Ffmiv Tree.sebitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenortl2).esviconfig.vmlIteration run Search HS.postman_collection.json--report(1).csvm licence hettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•Alfredv P1 mazanoko imaaoc. VWI6ана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.56 GB availabld• Inu 14 May 1/:20.24Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 4+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45240
|
1621
|
65
|
2026-05-14T14:26:30.711979+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768790711_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStorm•FileEditViewNavigateCodeLaravelRefactorRu PhpStorm•FileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl100% (C47FV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit K vHandleHubspotRateLimitTestv* :Projectv8• Thu 14 May 17:26:30Q© ReportController.php© AutomatedReportGenerated.php© TrackAutomatedReportGeneratedEvent.phpPlaybackController.php:= custom.logT SyncCrmEntitiesTrait.phpUserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpT SyncFieldsTrait.php() WriteCrmTrait.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php© ImportActivityTypes.phpT WriteCrmTrait.php xDecorateActivity.php> O Utils.© Service.phpT LogActivityTrait.php© PlainTextDecorateActivity.php© ActivityPlaybookTrait.phpCrmHelperRepository.php> D Webhook© AccountController.phpT IntegrationAppTrait.php=.env.stagingE.env© DetachActivityObject.php18© BatchSyncCollector.php© BatchSyncRedisService.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.phpHubspotPaginationService.php© Client.php© ClosedDealStagesService.pt19C DealFieldsService.php© DecorateActivity.php© FieldDefinitions.php© HandleHubspotRateLimit.phptrait WriteCrmTraitprivate function buildConferencePayload(Activity $activity): Activity'timestamp'=>$decorator->formatStartTime(Sactivity),'type' => 'MEETING',48 ×6 ^5562© FieldTypeConverter.php6364]:© HubspotClientinterface.php© HubspotTokenManager.php65© PayloadBuilder.php66© RemoteCrmObjectManipulat67if ($activity->hasActivityType() && $this-›matchesCrmType($activity->getActivityType())){$engagement['activityType'] = $activity->getActivityType()->getName();68ResponseNormalize.php© Service.php69© SyncFieldAction.php70© SyncRelatedActivityManage71© WebhookSyncBatchProcess72$metadata = ['meetingOutcome' => $decorator->convertMeetingOutcome($activity),'internalMeetingNotes' => $decorator->generateDescription(Sactivity),v D IntegrationApp73];> [J Accessors74> D Api75> D Config76D DTO7778C Filters79// Only if they are creating a fresh engagement should we update the times, otherwise it will causeif (! $activity->hasCrmProviderId)) {if (Sactivity->hasActualStartTime() && $activity->hasActualEndTime()) {Smetadata[ 'startTime'] = $decorator->formatStartTime(Sactivity);Smetadata[ 'endTime'] = $decorator->formatEndTime(Sactivity);D Jobs0 ProspectSearchStrategy80> D ServiceTraits81$metadata['title'] = $decorator->generateTitle($activity);82© DataClient.php© DecorateActivity.php8384© LocalSearch.php85// For now, this is just a "meetingOutcome" field for somecustomers.Smetadata = array_merge(Smetadata, $this->fetchCustomFieldData(Sactivity, Field::0BJECT_TASK));© LocalSearchinterface.php86A DamntaCaarch nhnWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]D211—212— 213|214215216217218219220-221222223224225226= 227— 228229238231232233V234Go jiminny0841A3 X4 A Vm migrations oim teams wherem crm_layouts !iM crm_layout_eiIM crm_fields Wim features;m team_feature:m opportunitie:m teams;1.id, CASEWHENidFROMsocial,1 on u.id= sa.:1..n<->1: on t.L_id = 1052 andIMaccounts wheiactivities81:53UTF-8Co 4 spaces...
|
NULL
|
-2589652595435948857
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhpStorm•FileEditViewNavigateCodeLaravelRefactorRu PhpStorm•FileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl100% (C47FV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit K vHandleHubspotRateLimitTestv* :Projectv8• Thu 14 May 17:26:30Q© ReportController.php© AutomatedReportGenerated.php© TrackAutomatedReportGeneratedEvent.phpPlaybackController.php:= custom.logT SyncCrmEntitiesTrait.phpUserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpT SyncFieldsTrait.php() WriteCrmTrait.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php© ImportActivityTypes.phpT WriteCrmTrait.php xDecorateActivity.php> O Utils.© Service.phpT LogActivityTrait.php© PlainTextDecorateActivity.php© ActivityPlaybookTrait.phpCrmHelperRepository.php> D Webhook© AccountController.phpT IntegrationAppTrait.php=.env.stagingE.env© DetachActivityObject.php18© BatchSyncCollector.php© BatchSyncRedisService.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.phpHubspotPaginationService.php© Client.php© ClosedDealStagesService.pt19C DealFieldsService.php© DecorateActivity.php© FieldDefinitions.php© HandleHubspotRateLimit.phptrait WriteCrmTraitprivate function buildConferencePayload(Activity $activity): Activity'timestamp'=>$decorator->formatStartTime(Sactivity),'type' => 'MEETING',48 ×6 ^5562© FieldTypeConverter.php6364]:© HubspotClientinterface.php© HubspotTokenManager.php65© PayloadBuilder.php66© RemoteCrmObjectManipulat67if ($activity->hasActivityType() && $this-›matchesCrmType($activity->getActivityType())){$engagement['activityType'] = $activity->getActivityType()->getName();68ResponseNormalize.php© Service.php69© SyncFieldAction.php70© SyncRelatedActivityManage71© WebhookSyncBatchProcess72$metadata = ['meetingOutcome' => $decorator->convertMeetingOutcome($activity),'internalMeetingNotes' => $decorator->generateDescription(Sactivity),v D IntegrationApp73];> [J Accessors74> D Api75> D Config76D DTO7778C Filters79// Only if they are creating a fresh engagement should we update the times, otherwise it will causeif (! $activity->hasCrmProviderId)) {if (Sactivity->hasActualStartTime() && $activity->hasActualEndTime()) {Smetadata[ 'startTime'] = $decorator->formatStartTime(Sactivity);Smetadata[ 'endTime'] = $decorator->formatEndTime(Sactivity);D Jobs0 ProspectSearchStrategy80> D ServiceTraits81$metadata['title'] = $decorator->generateTitle($activity);82© DataClient.php© DecorateActivity.php8384© LocalSearch.php85// For now, this is just a "meetingOutcome" field for somecustomers.Smetadata = array_merge(Smetadata, $this->fetchCustomFieldData(Sactivity, Field::0BJECT_TASK));© LocalSearchinterface.php86A DamntaCaarch nhnWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]D211—212— 213|214215216217218219220-221222223224225226= 227— 228229238231232233V234Go jiminny0841A3 X4 A Vm migrations oim teams wherem crm_layouts !iM crm_layout_eiIM crm_fields Wim features;m team_feature:m opportunitie:m teams;1.id, CASEWHENidFROMsocial,1 on u.id= sa.:1..n<->1: on t.L_id = 1052 andIMaccounts wheiactivities81:53UTF-8Co 4 spaces...
|
45239
|
NULL
|
NULL
|
NULL
|
|
45242
|
NULL
|
0
|
2026-05-14T14:26:31.422757+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768791422_m2.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.2962101,"top":1.0,"width":0.03856383,"height":-0.019952059},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.33477393,"top":1.0,"width":0.122340426,"height":-0.019952059},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.5731383,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.5884308,"top":1.0,"width":0.076130316,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.66456115,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.67586434,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.6871675,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.7150931,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.72639626,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.73769945,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3089888231946957708
|
-6979382010130101301
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preuiew in sact[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIAl Notes: OffLeavev Q SearchDate ModifiedYesterday at 13.34Yesterday at 13:34Yesterday at 13.33Yesterdav at 13:32resterday al i5.sYesterdav at 13:31Yesterday at 13:30Yesterday at 13.29Yesterdav at 13:29resterday at 15.2oYesterday at 13:27Yesterday at 13:26Yesterday at 13:26Yesterdav at 13:25Yesterday at 13.24Yesterdav at 13:24Yesterday at 13:23Yesterday at 13:21Voctorday at 12:21Yesterday at 13.20Yesterdav at 13:19Yesterdav at 13:19Yesterday at 13:18Yesterday at 13:18Yesterday at 13:17Yesterday at 13.16Yecterdav at 12:16Yesterdav at 13:15resterday at 13-10Yesterdav at 13:14Yesterday at 13:14Yesterday at 13:13Yecterdav at 12:12Yesterday at 13.11Yesterdav at 13:11Yesterday at 13:10Yesterday at 13:09Yoctorday at 12:00Yesterdav at 13:08Yesterday at 13.08Yecterdav at 12:07Yesterday at 13:07Yesterdav at 13:0617K:MPEG-4 movie29 KB6 KB12 Kb9 KBI7 KB8 KB37 K:MDEG-A movieMPEG-4 movieMPEG-4 movieMPEG-4 movie10 KB7 KBMDEG-A movieMPEG-4 movie9 KBI8 KB72 KB14 K:MPEG-4 movieMPEG-4 movie13 KBMDECA mavile9 KB18 KB|MPEG-4 movie12 KBI10 KB16 KBMPEG-1 movidMPEG-4 movie6KEMPEG-4 movie6 KB MPEG-4 movie12 KB23 K:MPEG-4 movie8 KB6 KBMDSG.A movicMPEG-4 movie11 KE11 KBMPEG-4 movieMPEG-4 movie20 KB34 K:MPEG-4 movie10kр7 KB5KBMDEG.A movidMPEG-4 movie11 KPMDEG-A movie26 KB MPEG-4 movie111 KBMPEG-4 movie102 K:88 KB59 KB98 KBMPEG-4 movieMDEC.A moviaMPEG-4 movie07 KPMDEG-A movie66 KBMPEG-4 movie44 KBMPEG-4 movie03 K:IMPEG-A movid78 KB MPEG-4 movie50 KB58 KB27 KPMPEG-4 movieMDEG.A movio7 KB12 KBMPEG-4 movie22 K:17 KBMDEC A movid19 KB32 K8MPEG-4 movieFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc tolde0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfP Kdf-a,k Ffmiv Tree.sebitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenortl2).esviconfig.vmlIteration run Search HS.postman_collection.json--report(1).csvm licence hettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•Alfredv P1 mazanoko imaaoc. VWI6ана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentK:PDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.56 GB availabld• Inu 14 May 1/:20.3Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 4+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
45238
|
NULL
|
NULL
|
NULL
|
|
45256
|
1623
|
7
|
2026-05-14T14:27:36.805803+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768856805_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl100% (C47•FV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTest* :8• Thu 14 May 17:27:36QProject v© ReportController.php© AutomatedReportGenerated.php© TrackAutomatedReportGeneratedEvent.phpPlaybackController.php:= custom.log© DecorateActivity.phpUserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.phpSendReportJob.php© LocalSearch.php• LocalSearchlnterface.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php© ImportActivityTypes.phpT WriteCrmTrait.php xDecorateActivity.php© RemoteSearch.php© Service.phpT LogActivityTrait.php© PlainTextDecorateActivity.php© ActivityPlaybookTrait.phpCrmHelperRepository.php© Service.php© AccountController.phpT IntegrationAppTrait.php=.env.stagingE.env© DetachActivityObject.php18v C Listeners© ConvertLeadActivities.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.phpHubspotPaginationService.php© PurgeLookupCache.php› D Metadata19> Migration› Pipedrivev O Salesforce© HandleHubspotRateLimit.phptrait WriteCrmTraitprivate function buildConferencePayload(Activity $activity): Activity'timestamp'=>$decorator->formatStartTime(Sactivity),'type' => 'MEETING',48 м6 ^556263› D Fields64]:> D OpportunityMatcher65> OpportunitySyncStrategy66> D ProspectSearchStrategy67if ($activity->hasActivityType() && $this-›matchesCrmType($activity->getActivityType())){$engagement['activityType'] = $activity->getActivityType()->getName();v D ServiceTraits68T BatchSyncTrait.php69D FollowupActivityTrait.php70& LogActivityTrait.php71T RecordManipulationsTrai72$metadata = ['meetingOutcome' => $decorator->convertMeetingOutcome($activity),'internalMeetingNotes' => $decorator->generateDescription(Sactivity),T SyncFieldsTrait.php73];© Client.php74© DecorateActivity.phpT DeleteObjectsTrait.php7576© FieldDefinitions.php77© PayloadBuilder.php78© Profile.php79// Only if they are creating a fresh engagement should we update the times, otherwise it will causeif (! $activity->hasCrmProviderId)) {if (Sactivity->hasActualStartTime() && $activity->hasActualEndTime()) {Smetadata[ 'startTime'] = $decorator->formatStartTime(Sactivity);Smetadata[ 'endTime'] = $decorator->formatEndTime(Sactivity);© QueryBuilder.php80© QueryHandler.php81$metadata['title'] = $decorator->generateTitle($activity);© Querylterator.php82© QueryResults.php83© Service.php84© SyncBatchRedisService.php85// For now, this is just a "meetingOutcome" field for somecustomers.Smetadata = array_merge(Smetadata, $this->fetchCustomFieldData(Sactivity, Field::0BJECT_TASK));D Traits86C. Raceflient nhnWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]D211—212— 213|214215216217218219220-221222223224225226= 227— 228229238231232233V234Go jiminny0841A3 X4 A Vm migrations oim teams wherem crm_layouts !iM crm_layout_eiIM crm_fields Wim features;m team_feature:m opportunitie:m teams;1.id, CASEWHENidFROMsocial,1 on u.id= sa.:1..n<->1: on t.L_id = 1052 andIMaccounts wheiactivities81:53UTF-8Co 4 spaces...
|
NULL
|
8831533670893284684
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl100% (C47•FV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTest* :8• Thu 14 May 17:27:36QProject v© ReportController.php© AutomatedReportGenerated.php© TrackAutomatedReportGeneratedEvent.phpPlaybackController.php:= custom.log© DecorateActivity.phpUserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.phpSendReportJob.php© LocalSearch.php• LocalSearchlnterface.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php© ImportActivityTypes.phpT WriteCrmTrait.php xDecorateActivity.php© RemoteSearch.php© Service.phpT LogActivityTrait.php© PlainTextDecorateActivity.php© ActivityPlaybookTrait.phpCrmHelperRepository.php© Service.php© AccountController.phpT IntegrationAppTrait.php=.env.stagingE.env© DetachActivityObject.php18v C Listeners© ConvertLeadActivities.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.phpHubspotPaginationService.php© PurgeLookupCache.php› D Metadata19> Migration› Pipedrivev O Salesforce© HandleHubspotRateLimit.phptrait WriteCrmTraitprivate function buildConferencePayload(Activity $activity): Activity'timestamp'=>$decorator->formatStartTime(Sactivity),'type' => 'MEETING',48 м6 ^556263› D Fields64]:> D OpportunityMatcher65> OpportunitySyncStrategy66> D ProspectSearchStrategy67if ($activity->hasActivityType() && $this-›matchesCrmType($activity->getActivityType())){$engagement['activityType'] = $activity->getActivityType()->getName();v D ServiceTraits68T BatchSyncTrait.php69D FollowupActivityTrait.php70& LogActivityTrait.php71T RecordManipulationsTrai72$metadata = ['meetingOutcome' => $decorator->convertMeetingOutcome($activity),'internalMeetingNotes' => $decorator->generateDescription(Sactivity),T SyncFieldsTrait.php73];© Client.php74© DecorateActivity.phpT DeleteObjectsTrait.php7576© FieldDefinitions.php77© PayloadBuilder.php78© Profile.php79// Only if they are creating a fresh engagement should we update the times, otherwise it will causeif (! $activity->hasCrmProviderId)) {if (Sactivity->hasActualStartTime() && $activity->hasActualEndTime()) {Smetadata[ 'startTime'] = $decorator->formatStartTime(Sactivity);Smetadata[ 'endTime'] = $decorator->formatEndTime(Sactivity);© QueryBuilder.php80© QueryHandler.php81$metadata['title'] = $decorator->generateTitle($activity);© Querylterator.php82© QueryResults.php83© Service.php84© SyncBatchRedisService.php85// For now, this is just a "meetingOutcome" field for somecustomers.Smetadata = array_merge(Smetadata, $this->fetchCustomFieldData(Sactivity, Field::0BJECT_TASK));D Traits86C. Raceflient nhnWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]D211—212— 213|214215216217218219220-221222223224225226= 227— 228229238231232233V234Go jiminny0841A3 X4 A Vm migrations oim teams wherem crm_layouts !iM crm_layout_eiIM crm_fields Wim features;m team_feature:m opportunitie:m teams;1.id, CASEWHENidFROMsocial,1 on u.id= sa.:1..n<->1: on t.L_id = 1052 andIMaccounts wheiactivities81:53UTF-8Co 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45257
|
1623
|
8
|
2026-05-14T14:27:40.143142+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768860143_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project
New File or Directory…...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.054166667,"top":0.027777778,"width":0.08055556,"height":0.035555556},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.13472222,"top":0.027777778,"width":0.25555557,"height":0.035555556},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.6326389,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.6645833,"top":0.027777778,"width":0.15902779,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.82361114,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.8472222,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.87083334,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9291667,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9527778,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9763889,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"bounds":{"left":0.78055555,"top":0.3122222,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.80138886,"top":0.3122222,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.8215278,"top":0.31,"width":0.015277778,"height":0.025555555},"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.8368056,"top":0.31,"width":0.014583333,"height":0.025555555},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\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.85486114,"top":0.31,"width":0.018055556,"height":0.026666667},"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.87291664,"top":0.31,"width":0.018055556,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.9138889,"top":0.31,"width":0.059027776,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"bounds":{"left":0.8645833,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.8854167,"top":0.34444445,"width":0.015277778,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.9048611,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.92569447,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9458333,"top":0.3422222,"width":0.015277778,"height":0.025555555},"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.9611111,"top":0.3422222,"width":0.014583333,"height":0.025555555},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","depth":4,"on_screen":true,"value":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.025,"top":0.06666667,"width":0.050694443,"height":0.034444444},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-2316546239645567936
|
6551498163460912845
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project
New File or Directory…...
|
45256
|
NULL
|
NULL
|
NULL
|
|
45259
|
1623
|
9
|
2026-05-14T14:27:44.274066+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768864274_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelp•FV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit k vlhl100% (C47HandleHubspotRateLimitTestv8• Thu 14 May 17:27:44* :QProjectv© ReportController.php© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpPlaybackController.php= custom.logT SyncCrmEntitiesTrait.phpUserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpT SyncFieldsTrait.php() WriteCrmTrait.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php© ImportActivityTypes.phpT WriteCrmTrait.phpDecorateActivity.php> O Utils.© Service.phpT LogActivityTrait.php© PlainTextDecorateActivity.php© ActivityPlaybookTrait.phpCrmHelperRepository.php> D Webhook© AccountController.phpT IntegrationAppTrait.php=.env.stagingE.env© DetachActivityObject.php18.••© BatchSyncCollector.php© BatchSyncRedisService.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.phpHubspotPaginationService.php© Client.php© ClosedDealStagesService.pt19C DealFieldsService.php55© DecorateActivity.php© FieldDefinitions.php© FieldTypeConverter.php62© HubspotClientinterface.php© HandleHubspotRateLimit.phptrait WriteCrmTraitprivate function buildConferencePayload(Activity $activity): Activity'active' = true,'ownerId'= Sthis->profile->getCrmProviderIdO,'timestamp' => $decorator->formatStartTime($activity),'type' => 'MEETING',88 ×6 л606163© HubspotTokenManager.php64]:© PayloadBuilder.php65© RemoteCrmObjectManipulat66ResponseNormalize.php67if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())){$engagement['activityType'] = $activity->getActivityType()->getName();© Service.php68© SyncFieldAction.php69© SyncRelatedActivityManage70© WebhookSyncBatchProcess71v D IntegrationApp72> [J Accessors$metadata = ['meetingOutcome' => $decorator->convertMeetingOutcome(Sactivity),'internalMeetingNotes'= $decorator->generateDescription(Sactivity),731:> D Api74> D Config75D DTO76> O Filters77D Jobs78> C ProspectSearchStrategy79// Only if they are creating a fresh engagement should we update the times, otherwise it will causeif (! $activity->hasCrmProviderIdO) {if (Sactivity->hasActualStartTime() && $activity->hasActualEndTime()) €Smetadata['startTime'] = $decorator->formatStartTime(Sactivity):Smetadata[ 'endTime'] = $decorator->formatEndTime($activity);> D ServiceTraits80© DataClient.php81$metadata['title'] = $decorator->generateTitle($activity);© DecorateActivity.php82© LocalSearch.php83© LocalSearchinterface.php84field for someA DamntaCaarch nhn// For now, this is just a "meetingOutcome"D211-212— 213|214215216217218219220-221222223224225226= 227— 228229230231232233 V234customers.Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W« console [QAI PROD) X4 console [PROD]A console (EU]Go jiminny0841A3 X4 A Vm migrations oim teams wherem crm_layouts !iM crm_layout_eiIM crm_fields Wim features;m team_feature:m opportunitie:m teams;1.id, CASEWHENidFROMsocial,I on u.id= sa.:1..n<->1: on t.L_id = 1052 andIMaccounts wheiactivities81:51UTF-8Co 4 spaces...
|
NULL
|
-1240337411389915602
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelp•FV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit k vlhl100% (C47HandleHubspotRateLimitTestv8• Thu 14 May 17:27:44* :QProjectv© ReportController.php© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpPlaybackController.php= custom.logT SyncCrmEntitiesTrait.phpUserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpT SyncFieldsTrait.php() WriteCrmTrait.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php© ImportActivityTypes.phpT WriteCrmTrait.phpDecorateActivity.php> O Utils.© Service.phpT LogActivityTrait.php© PlainTextDecorateActivity.php© ActivityPlaybookTrait.phpCrmHelperRepository.php> D Webhook© AccountController.phpT IntegrationAppTrait.php=.env.stagingE.env© DetachActivityObject.php18.••© BatchSyncCollector.php© BatchSyncRedisService.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.phpHubspotPaginationService.php© Client.php© ClosedDealStagesService.pt19C DealFieldsService.php55© DecorateActivity.php© FieldDefinitions.php© FieldTypeConverter.php62© HubspotClientinterface.php© HandleHubspotRateLimit.phptrait WriteCrmTraitprivate function buildConferencePayload(Activity $activity): Activity'active' = true,'ownerId'= Sthis->profile->getCrmProviderIdO,'timestamp' => $decorator->formatStartTime($activity),'type' => 'MEETING',88 ×6 л606163© HubspotTokenManager.php64]:© PayloadBuilder.php65© RemoteCrmObjectManipulat66ResponseNormalize.php67if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())){$engagement['activityType'] = $activity->getActivityType()->getName();© Service.php68© SyncFieldAction.php69© SyncRelatedActivityManage70© WebhookSyncBatchProcess71v D IntegrationApp72> [J Accessors$metadata = ['meetingOutcome' => $decorator->convertMeetingOutcome(Sactivity),'internalMeetingNotes'= $decorator->generateDescription(Sactivity),731:> D Api74> D Config75D DTO76> O Filters77D Jobs78> C ProspectSearchStrategy79// Only if they are creating a fresh engagement should we update the times, otherwise it will causeif (! $activity->hasCrmProviderIdO) {if (Sactivity->hasActualStartTime() && $activity->hasActualEndTime()) €Smetadata['startTime'] = $decorator->formatStartTime(Sactivity):Smetadata[ 'endTime'] = $decorator->formatEndTime($activity);> D ServiceTraits80© DataClient.php81$metadata['title'] = $decorator->generateTitle($activity);© DecorateActivity.php82© LocalSearch.php83© LocalSearchinterface.php84field for someA DamntaCaarch nhn// For now, this is just a "meetingOutcome"D211-212— 213|214215216217218219220-221222223224225226= 227— 228229230231232233 V234customers.Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W« console [QAI PROD) X4 console [PROD]A console (EU]Go jiminny0841A3 X4 A Vm migrations oim teams wherem crm_layouts !iM crm_layout_eiIM crm_fields Wim features;m team_feature:m opportunitie:m teams;1.id, CASEWHENidFROMsocial,I on u.id= sa.:1..n<->1: on t.L_id = 1052 andIMaccounts wheiactivities81:51UTF-8Co 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45260
|
1624
|
7
|
2026-05-14T14:27:44.274004+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768864274_m2.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.2962101,"top":1.0,"width":0.03856383,"height":-0.019952059},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.33477393,"top":1.0,"width":0.122340426,"height":-0.019952059},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.5731383,"top":1.0,"width":0.011303191,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.5884308,"top":1.0,"width":0.076130316,"height":-0.019952059},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7058342144550237105
|
-7195274138713862717
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Jiminny... ~ActivityFilesLater@ jiminny-x-integrati• plattorm-inner-teamE Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity lab# engineering# general#jiminny-bgac nlattorm-nckets# product launches# random# releases# sofia-officed sunport# thank-yous# the people of iimi... Direct messages• 8la 62% Galya Dimitrovavasil VasilevM Stefka StovanovaSg: Todor StamatovMario GeorgievNiudlay lanay. James Graham "* Stoyan Tanevo Stelivan Georgiev( Petko Kashinski*. Lukas Kovali...::: AnnsToastS lira Gloud6d Huddle with Aneliva Angelova* Aneliya Angelova &• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някъде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preuiew in sact[HubSpotl Optimise CRM rematct(7 OpenRosdy for OA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira* SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle (LIVE 4:06 PMAneliva Angelova is here toolAneliva Angelova 4:44 PN11512582Lukas Kovalik M 5.16 PNcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-vne' annlication/ison'--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIr7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIAl Notes: OffLeavev Q SearchDate ModifiedYesterdav at 13.35Yesterday at 13:35Yesterday at 13.34Yesterdav at 13:33Yesterday at 13:32Yesterdav at 13:37Yesterday at 13:31Yesterday at 13.30Yesterdav at 13:30Yesterday at 13:29resterday at 15.4gYesterdav at 13:28Yesterday at 13:28Yesterday at 13:27Yesterdav at 13:26resterday at 13-4oYesterdav at 13:25Yesterday at 13:24Yesterday at 13:24Yesterday at 13:23Voctorday at 12:22Yesterday at 13.21Yesterdav at 13:20Yesterdav at 13:20Yesterday at 13:19Yesterday at 13:19Yesterday at 13:18Yesterday at 13.17Yecterdav at 12:17Yesterdav at 13:16Yesterday at 13-10Yesterdav at 13:15Yesterday at 13:15Yesterday at 13:14Yecterdav at 12:12Yesterday at 13.12Yesterdav at 13:12Yesterday at 13:11Yesterday at 13:10Yoctorday at 12:10Yesterdav at 13:09Yesterday at 13.09Yocterdav at 12:081Yesterdav at 13:08Yesterdav at 13:071S K:MPEG-4 movie16 KB17 KB29 Kb6 KE12 KB9 KB7 K:MDEG-A movieMPEG-4 movieMPEG-4 movieMPE0"4 movie8 KBMDSG-A movie37 KB10 KbMP2G-4 movie7 KBI8 KBMPEG-4 movieMPEG-4 movie8 K:72 KB14 KE13 K:MoECh movioMPEG-4 movieO KRI18 KB12 KBMPEG-1 movidMPEG-4 movie10 KEMPEG-4 movie16 KB MPEG-4 movie6 KB6 KB1MPEG-4 movie12 KB23 KB8 KBMDSG.A movicMPEG-4 movie6KE6 KBMPEG-4 movieMPEG-4 movie11 KB11 K:MPEG-4 movie20 kRMDEC A movid10 KBMPEG-4 movie7 KPMDEG-A movie5 KBMPEG-4 movie11 KBMPEG-4 movie26 KB111 KB102 KB88 KBMPEG-4 movieMDEC.A movialMPEG-4 movieKO KRMDEG-A movie98 KBMPEG-4 movie97 KBMPEG-4 movie66 K:MPEG-A movie44 KB MPEG-4 movie93 KB78 KB50 kpMPEG-4 movieMDEG.A movio58 KB27 KBMPEG-4 movie7KP12 KBMADECA movid32 KB17 K8MPEG-4 movieFavourites• jiminny• Recents* ApplicationsiCloud• iCloud Drive992 Svnc tolde0 DXP4800PLUS-B5F49 Networl• CRMI• Orange• Red|• Yellow• Green• Purple•) All lags..IhlDownloadsNameLoom.pkgAlfred copv.alfredoreterences=KeychronAssist-1.0.2 (1).dmeKeychronAssist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zip• Transcript.pd→mage U.loge1 Orioninstaller.dma_image.jpg- image (2).1pc• ПO-22221726037035-004-001_ORGES.pdf%D0%9F%D0%9E-22221726037035-004-001 archive.zipПO-22221726037035-004-001_archive (1).zipreport(2).xmlAltred copy2.altredoreterencesСE 060209С О000000026571172 CWICT 0Р70501260015900 ndt= 27022026_0000000026574472_ SWIFT_ [IBAN].pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001reportxmmi=pdt.odiB pdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfKoválik Family Tree.gedbitwarden export 20251031122528.isonlKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst aid_notes_complete.docxrenortl2).esv1 config.vmlIteration run Search HS.postman_collection.jsonm licence hettertouchtoalMariusHosting Config.isonnokc.901a6502.6667.A62h-062 ccu•AlfredwPmazanoko.imnaoc.YWIfана Ковалик.jpg•искане даниел Ковалик..pg• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик iро• Dhotac 2.001Q SearchKind00,4 MD55.9 MBinstdlle..dckageAlfred...ferences10,1 MB10,1 MBIL MBDisk ImageDisk ImageLiP archive6.6 MBZIp archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDE Document140 KbZIP archive148 KВ148 KB122 KBZIP archivelZIP archiveXML document111 KBAlfred...ferences94 KBDDE Documont92 KBPDr DocumentPDF Document91 KB91 KBXML document30 KBn0kpDDE Nocumont28 KBPDF Document28 KRPDE Document28 KB27 KBDocument14 KB11 KB6 KB6KBJSONINCV NacumontZIP archive5KRMarkdo…..umentWindo...n image4 KB3 KBword ..cumentCSV Document2 KBVAMI dosumon1 KB029 buteccSV Documenthttlicence183 bytesZero butesJSONAlfrod foronso!Zero bytesFolde1.9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.55 GB availabld• Inu 14 Mау 1/•2/.43)Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 ,Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3712 Goh 2006 9t 11:5 A1Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nau 2025 4+ 17:506 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0419 Mar 2026 at 11:55|10 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:362616 Oct 2025 aт 16:0122 Aar 2026 at 12:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45261
|
1623
|
10
|
2026-05-14T14:27:47.498853+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768867498_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl100% (C47•FV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit K vHandleHubspotRateLimitTestv8• Thu 14 May 17:27:47* :QProjectv© ReportController.php© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpPlaybackController.php= custom.logT SyncCrmEntitiesTrait.phpUserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpT SyncFieldsTrait.php() WriteCrmTrait.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php© ImportActivityTypes.phpT WriteCrmTrait.phpDecorateActivity.php> O Utils.© Service.phpT LogActivityTrait.php© PlainTextDecorateActivity.php© ActivityPlaybookTrait.phpCrmHelperRepository.php> D Webhook© AccountController.phpT IntegrationAppTrait.php=.env.stagingE.env© DetachActivityObject.php18© BatchSyncCollector.php© BatchSyncRedisService.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.phpHubspotPaginationService.php© Client.php© ClosedDealStagesService.pt19C DealFieldsService.php55© DecorateActivity.php© FieldDefinitions.php© FieldTypeConverter.php62© HubspotClientinterface.php© HandleHubspotRateLimit.phptrait WriteCrmTraitprivate function buildConferencePayload(Activity $activity): Activity'active' = true,'ownerId'= Sthis->profile->getCrmProviderIdO,'timestamp' => $decorator->formatStartTime($activity),'type' => 'MEETING',88 ×6 л606163© HubspotTokenManager.php64]:© PayloadBuilder.php65© RemoteCrmObjectManipulatResponseNormalize.php6667if ($activity->hasActivityType() && $this->matchesChmType($activity->getActivityType())) {$engagement['activityType'] = $activity->getActivityType() ->getName();© Service.php68© SyncFieldAction.php69© SyncRelatedActivityManage70© WebhookSyncBatchProcess71v D IntegrationApp72> [J Accessors$metadata = ['meetingOutcome' => $decorator->convertMeetingOutcome($activity),'internalMeetingNotes'= $decorator->generateDescription(Sactivity),731:> D Api74> D Config75D DTO76> O Filters77D Jobs780 ProspectSearchStrategy79// Only if they are creating a fresh engagement should we update the times, otherwise it willcauseif (! $activity->hasCrmProviderIdO) {if (Sactivity->hasActualStartTime() && $activity->hasActualEndTime()) €Smetadata['startTime'] = $decorator->formatStartTime(Sactivity):Smetadata[ 'endTime'] = $decorator->formatEndTime($activity);> D ServiceTraits80© DataClient.php81Smetadata['title'] = $decorator->generateTitle($activity):© DecorateActivity.php82© LocalSearch.php83© LocalSearchinterface.php84A DamntaCaarch nhn// For now, this is just a "meetingOutcome"field for somecustomers.Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W« console [QAI PROD) X4 console [PROD]A console (EU]D211-212— 213|214215216217218219220-221222223224225226= 227— 228229230— 231232233 V234Go jiminny0841A3 X4 A Vm migrations oim teams wherem crm_layouts !iM crm_layout_eiIM crm_fields Wim features;m team_feature:m opportunitie:m teams;1.id, CASEWHENidFROMsocial,on u.id= sa.:1..n<->1: on t.L_id = 1052 andIMaccounts wheiactivities66:60UTF-8Co 4 spaces...
|
NULL
|
19990527998985743
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl100% (C47•FV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit K vHandleHubspotRateLimitTestv8• Thu 14 May 17:27:47* :QProjectv© ReportController.php© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.phpPlaybackController.php= custom.logT SyncCrmEntitiesTrait.phpUserAutomatedReportsController.phpPlanhatService.phpAutomatedReportResult.phpSendReportJob.phpT SyncFieldsTrait.php() WriteCrmTrait.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php© ImportActivityTypes.phpT WriteCrmTrait.phpDecorateActivity.php> O Utils.© Service.phpT LogActivityTrait.php© PlainTextDecorateActivity.php© ActivityPlaybookTrait.phpCrmHelperRepository.php> D Webhook© AccountController.phpT IntegrationAppTrait.php=.env.stagingE.env© DetachActivityObject.php18© BatchSyncCollector.php© BatchSyncRedisService.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.phpHubspotPaginationService.php© Client.php© ClosedDealStagesService.pt19C DealFieldsService.php55© DecorateActivity.php© FieldDefinitions.php© FieldTypeConverter.php62© HubspotClientinterface.php© HandleHubspotRateLimit.phptrait WriteCrmTraitprivate function buildConferencePayload(Activity $activity): Activity'active' = true,'ownerId'= Sthis->profile->getCrmProviderIdO,'timestamp' => $decorator->formatStartTime($activity),'type' => 'MEETING',88 ×6 л606163© HubspotTokenManager.php64]:© PayloadBuilder.php65© RemoteCrmObjectManipulatResponseNormalize.php6667if ($activity->hasActivityType() && $this->matchesChmType($activity->getActivityType())) {$engagement['activityType'] = $activity->getActivityType() ->getName();© Service.php68© SyncFieldAction.php69© SyncRelatedActivityManage70© WebhookSyncBatchProcess71v D IntegrationApp72> [J Accessors$metadata = ['meetingOutcome' => $decorator->convertMeetingOutcome($activity),'internalMeetingNotes'= $decorator->generateDescription(Sactivity),731:> D Api74> D Config75D DTO76> O Filters77D Jobs780 ProspectSearchStrategy79// Only if they are creating a fresh engagement should we update the times, otherwise it willcauseif (! $activity->hasCrmProviderIdO) {if (Sactivity->hasActualStartTime() && $activity->hasActualEndTime()) €Smetadata['startTime'] = $decorator->formatStartTime(Sactivity):Smetadata[ 'endTime'] = $decorator->formatEndTime($activity);> D ServiceTraits80© DataClient.php81Smetadata['title'] = $decorator->generateTitle($activity):© DecorateActivity.php82© LocalSearch.php83© LocalSearchinterface.php84A DamntaCaarch nhn// For now, this is just a "meetingOutcome"field for somecustomers.Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)W Windsurf Teams=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W« console [QAI PROD) X4 console [PROD]A console (EU]D211-212— 213|214215216217218219220-221222223224225226= 227— 228229230— 231232233 V234Go jiminny0841A3 X4 A Vm migrations oim teams wherem crm_layouts !iM crm_layout_eiIM crm_fields Wim features;m team_feature:m opportunitie:m teams;1.id, CASEWHENidFROMsocial,on u.id= sa.:1..n<->1: on t.L_id = 1052 andIMaccounts wheiactivities66:60UTF-8Co 4 spaces...
|
45259
|
NULL
|
NULL
|
NULL
|
|
45262
|
1624
|
8
|
2026-05-14T14:27:48.944434+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778768868944_m2.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
ActivityFilesLaterJiminny... ~@ jiminny-x-integrat ActivityFilesLaterJiminny... ~@ jiminny-x-integrati& platform-inner-team© Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity_lab# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the people of iimi..• Direct messages8. A...• A3 62P. Galya Dimitrova Mvasil VasilevA. Stefka Stoyanova%: Todor Stamatovf. Mario GeorgievP. Nikolay Ivanov2o James Graham "2. Stoyan Tanev. Steliyan Georgiev. Petko Kashinski*. Lukas Kovali...a: Apps® ToastS lira Gloud6 Huddle with Aneliya Angelova& R. Aneliya Angelova •• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някьде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova @ 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preview insackt[HubSpot] Optimise CRM rematctC OpenReady for QA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira+ SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle LIVE 4:06 PMAneliva Angelova is here toolAneliya Angelova 4:44 PM11512582Lukas Kovalik O 5:16 PMcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-Type: application/json'\--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIґ7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pm~ (Q SearchDate ModifiedYesterdav at 13.36Yesterday at 13:35Yesterday at 13.34Yesterday at 13:34Yesterday at 13:33resterday at 15.5sYesterdav at 13:37Yesterday at 13:32Yesterday ar 13.31Yesterday at 13:30Yesterday at 13:30resterday at 15.4gYesterdav at 13:29Yesterday at 13:28Yesterday at 13:28Yesterday at 13:27Yesterday at 13:27resterday at 13-40Yesterdav at 13:25Yesterday at 13:25Yesterday at 13:24Yesterday at 13:24Yesterday at 13:23Yesterday at 13.21Yesterdav at 13:21Yesterday at 13:20Yesterday at 13:20Yesterday at 13:19Yesterday at 13:19Yesterday at 13.18Vecterdav at 12:17Yesterday at 13:16Yesterdav at 13:16Yesterday at 13:15Yesterday at 13:14Yecterdav at 12:1/Yesterday at 13:13Yesterday at 13.13Yecterdav at 12:12Yesterday at 13:12Yesterday at 13:11Yesterday at 13:10Yesterday at 13:10Yesterday at 13.09Yecterdav at 12:091Yesterday at 13:08Yesterdav at 13:07MPEG-4 movie13 KBMPEG-4 movie16 KB1/ Kb29 KBMPEG-4 movieMPEG-4 movie6 KB12 KBMPE0"4 movie9 K:7 KBMPEG-4 movie8 KB37 KBMP2G-4 movie10 KBMPEG-4 movie7 KBMPEG"4 movie9K:8KBMPEG-4 movie72 KB14 K8|13 KRIMPEG-4 movieMPEG-4 movie9 KB18 KBMPEG-4 movie12 KEMPEG-4 movie10 KBMPEG-4 movie16 KB6 KB1MPEG-4 movie6 KBMPEG-4 movie12 KB23 KBMPEG-4 movie8 KEMPEG-4 movie6 KBMPEG-4 movie6 KBMPEG-4 movie11 K:MPEG-4 movie11 KBMPEG-4 movie34 KBMPEG-4 movie10 KPMPEG-4 movie7 KBMPEG-4 movie5 KBMPEG-4 movie11 KBIMPEG-4 movie26 KBMPEG-4 movie111 KB102 KBRR KRMPEG-4 movieMDEG-A movie59 KB98 KBMPEG-4 movie07 K:MPEG-A movie66 KB MPEG-4 movie44 KB93 KB79 KpMPEG-4 movieMPEG-4 movie58 KBMPEG-4 movie27 KP7 KBMPEG-4 movie12 KB32 K8MPEG-4 movieAl Notes: OffLeave• 0FavouritesE jiminny© Recents* ApplicationsiCloudiCloud Drive228 Sync folderQ DXP4800PLUS-B5FA@ Network|• CRMI• Orange• Red• Yellow• Green• Purple•) All lags..lohlDownloadsNameLoom.pkgAlfred copv.alfredoreterencesB KeychronAssist-1.0.2 (1).dmgA Keychron Assist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zipD Transcript.pdf→mage U.loge1 Orioninstaller.dmaimage.jpg- image (2).1pc• ПО-22221726037035-004-001_ORGES.pdf• %D0%9F%D0%9E-22221726037035-004-001_archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterences05012026_0000000026574472_ SWIFT_OB70501260015890.pdf27022026_0000000026574472_SWIFT_OB72702260049200.pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001B reporti) xm=pdt.odipdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfKoválik Family Tree.gedbitwarden export 20251031122528.isonKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst_aid_notes_complete.docxrenortl2).esvconfig.ymlIteration run Search HS.postman_collection.jsonm licence bettertouchtoalMariusHosting Config.json1ooks-891a6503-bbb7-4b2b-9c3.csv•Alfredmazanoke-images-YWJ6ана Ковалик.jpg•искане даниел Ковалик..pa• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик.jpg• Dhotac 2.001Q SearchKind00,4 MDinstdlle..dckage55.9 MBAlfred...ferences10,1 MBDisk Image10,1 MBDisk ImageIL MBLiP archive6,6 MBZIP archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDF Document140 KbZIP archive148 KВ148 KB122 KB111 KB94 KBZIP archivelZIP archiveXML documentAlfred...ferencesPDF Document92 KBPDr DocumentK:91 KB91 KB30 KB29 KBPDF DocumentXML documentPDF Document28 KBPDF Document28 KR28 KB27 KB14 KB11 KB6 KB6KBPDE DocumentDocumentJSONICSV DocumentZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBYAML document1 KBcSV Document928 byteshttlicence183 bytesZero butesJSONAlfred.. ferencesZero bytesFolde1,9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.54 GB availabld• Inu 14 Mау 1/•21.40Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3713 Feb 2026 at 11:54Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nov 2025 at 17:596 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0418 Mar 2026 at 11:5510 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:3630 Jan 2026 at 12:3616 Oct 2025 aт 16:0123 Apr 2026 at 13:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
-5464364581231200547
|
NULL
|
click
|
ocr
|
NULL
|
ActivityFilesLaterJiminny... ~@ jiminny-x-integrat ActivityFilesLaterJiminny... ~@ jiminny-x-integrati& platform-inner-team© Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity_lab# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the people of iimi..• Direct messages8. A...• A3 62P. Galya Dimitrova Mvasil VasilevA. Stefka Stoyanova%: Todor Stamatovf. Mario GeorgievP. Nikolay Ivanov2o James Graham "2. Stoyan Tanev. Steliyan Georgiev. Petko Kashinski*. Lukas Kovali...a: Apps® ToastS lira Gloud6 Huddle with Aneliya Angelova& R. Aneliya Angelova •• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някьде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova @ 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preview insackt[HubSpot] Optimise CRM rematctC OpenReady for QA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira+ SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle LIVE 4:06 PMAneliva Angelova is here toolAneliya Angelova 4:44 PM11512582Lukas Kovalik O 5:16 PMcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-Type: application/json'\--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIґ7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pm~ (Q SearchDate ModifiedYesterdav at 13.36Yesterday at 13:35Yesterday at 13.34Yesterday at 13:34Yesterday at 13:33resterday at 15.5sYesterdav at 13:37Yesterday at 13:32Yesterday ar 13.31Yesterday at 13:30Yesterday at 13:30resterday at 15.4gYesterdav at 13:29Yesterday at 13:28Yesterday at 13:28Yesterday at 13:27Yesterday at 13:27resterday at 13-40Yesterdav at 13:25Yesterday at 13:25Yesterday at 13:24Yesterday at 13:24Yesterday at 13:23Yesterday at 13.21Yesterdav at 13:21Yesterday at 13:20Yesterday at 13:20Yesterday at 13:19Yesterday at 13:19Yesterday at 13.18Vecterdav at 12:17Yesterday at 13:16Yesterdav at 13:16Yesterday at 13:15Yesterday at 13:14Yecterdav at 12:1/Yesterday at 13:13Yesterday at 13.13Yecterdav at 12:12Yesterday at 13:12Yesterday at 13:11Yesterday at 13:10Yesterday at 13:10Yesterday at 13.09Yecterdav at 12:091Yesterday at 13:08Yesterdav at 13:07MPEG-4 movie13 KBMPEG-4 movie16 KB1/ Kb29 KBMPEG-4 movieMPEG-4 movie6 KB12 KBMPE0"4 movie9 K:7 KBMPEG-4 movie8 KB37 KBMP2G-4 movie10 KBMPEG-4 movie7 KBMPEG"4 movie9K:8KBMPEG-4 movie72 KB14 K8|13 KRIMPEG-4 movieMPEG-4 movie9 KB18 KBMPEG-4 movie12 KEMPEG-4 movie10 KBMPEG-4 movie16 KB6 KB1MPEG-4 movie6 KBMPEG-4 movie12 KB23 KBMPEG-4 movie8 KEMPEG-4 movie6 KBMPEG-4 movie6 KBMPEG-4 movie11 K:MPEG-4 movie11 KBMPEG-4 movie34 KBMPEG-4 movie10 KPMPEG-4 movie7 KBMPEG-4 movie5 KBMPEG-4 movie11 KBIMPEG-4 movie26 KBMPEG-4 movie111 KB102 KBRR KRMPEG-4 movieMDEG-A movie59 KB98 KBMPEG-4 movie07 K:MPEG-A movie66 KB MPEG-4 movie44 KB93 KB79 KpMPEG-4 movieMPEG-4 movie58 KBMPEG-4 movie27 KP7 KBMPEG-4 movie12 KB32 K8MPEG-4 movieAl Notes: OffLeave• 0FavouritesE jiminny© Recents* ApplicationsiCloudiCloud Drive228 Sync folderQ DXP4800PLUS-B5FA@ Network|• CRMI• Orange• Red• Yellow• Green• Purple•) All lags..lohlDownloadsNameLoom.pkgAlfred copv.alfredoreterencesB KeychronAssist-1.0.2 (1).dmgA Keychron Assist-1.0.2.dmgmazanoke-images-yWJo.zioPhotos-3-001.zipD Transcript.pdf→mage U.loge1 Orioninstaller.dmaimage.jpg- image (2).1pc• ПО-22221726037035-004-001_ORGES.pdf• %D0%9F%D0%9E-22221726037035-004-001_archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterences05012026_0000000026574472_ SWIFT_OB70501260015890.pdf27022026_0000000026574472_SWIFT_OB72702260049200.pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001B reporti) xm=pdt.odipdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfKoválik Family Tree.gedbitwarden export 20251031122528.isonKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst_aid_notes_complete.docxrenortl2).esvconfig.ymlIteration run Search HS.postman_collection.jsonm licence bettertouchtoalMariusHosting Config.json1ooks-891a6503-bbb7-4b2b-9c3.csv•Alfredmazanoke-images-YWJ6ана Ковалик.jpg•искане даниел Ковалик..pa• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик.jpg• Dhotac 2.001Q SearchKind00,4 MDinstdlle..dckage55.9 MBAlfred...ferences10,1 MBDisk Image10,1 MBDisk ImageIL MBLiP archive6,6 MBZIP archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDF Document140 KbZIP archive148 KВ148 KB122 KB111 KB94 KBZIP archivelZIP archiveXML documentAlfred...ferencesPDF Document92 KBPDr DocumentK:91 KB91 KB30 KB29 KBPDF DocumentXML documentPDF Document28 KBPDF Document28 KR28 KB27 KB14 KB11 KB6 KB6KBPDE DocumentDocumentJSONICSV DocumentZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBYAML document1 KBcSV Document928 byteshttlicence183 bytesZero butesJSONAlfred.. ferencesZero bytesFolde1,9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.54 GB availabld• Inu 14 Mау 1/•21.40Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3713 Feb 2026 at 11:54Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nov 2025 at 17:596 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0418 Mar 2026 at 11:5510 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:3630 Jan 2026 at 12:3616 Oct 2025 aт 16:0123 Apr 2026 at 13:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
45260
|
NULL
|
NULL
|
NULL
|
|
45409
|
1625
|
48
|
2026-05-14T14:35:18.551533+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769318551_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelpablFV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit K vHandleHubspotRateLimitTest v100% <8• Thu 14 May 17:35:18* :QProject v+.•Х:-© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.phpwmieauonv PipedrivePlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php> C OpportunitySyncStrategy© ImportActivityTypes.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.php xT LogActivityTrait.php> D ProspectSearchStrategy© Pipedrive/Service.phpClose/Service.phpCopper/Service.phpBullhornService.phpPlainTextDecorateActivity.php© ApiFields.php© Client.phpT ActivityPlaybookTrait.phpCrmHelperRepository.php© AccountController.php=.env.staging=.env18© FieldDefinitions.php© DetachActivityObject.php© PipedriveApiClient.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.php© PipedriveApiException.php© HubspotPaginationService.php© HandleHubspotRateLimit.php© Service.php80classService extends BaseService implements© TokenStorage.php135A11 A130 X3 X21 ^private OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;v D Salesforce136> • Fields137 ₫public function__construct(> OpportunityMatcher138Client Sclient,> D OpportunitySyncStrategy139PayloadBuilder $payloadBuilder,> D ProspectSearchStrategy140 đprotected Dispatcher $eventDispatcher,v D ServiceTraits141private readonly CountriesMap $countriesMap,T BatchSyncTrait.php142private readonly ProspectPhotoPathService $prospectPhotoPathService,( FollowupActivityTrait.php143) f...3T LogActivityTrait.php156+ RecordManipulationsTrait.p157 >public function getDisplayName(): string{...}T SyncFieldsTrait.php161© Client.php162 ›public function getJobDelay(): intf.© DecorateActivity.php166G DeleteObjectsTrait.php167 đ >protected function get0AuthAccount(User $user): ?SocialAccount{...}© FieldDefinitions.php171© PayloadBuilder.php172 € >public function verifyTaskExists(Activity $activity): bool{...}© Profile.php207© QueryBuilder.php208 0 >public function query(string $queryToRun, array $parameters = []): QueryIterator{...}© QueryHandler.php213© Querylterator.php214/*===Organization Information ======© QueryResults.php215© Service.php216/**© SyncBatchRedisService.php217* Get a list of all the API Versions for the instance.Traits218*© BaseClient.php219* Athrows CrmExceptionWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17):=custom.log= laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]DGo jiminny084143 ×4 л215•m_layout_id =2162,1661,66799,66id = 33;— 220221222$223224225226227228229- 230— 231- 232- 233 V234id THEN •(ownhubspot':= 11512582;I I11W Windsurf Teams100:25UTF-8Ca 4 spaces...
|
NULL
|
-5507277541688491363
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelpablFV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit K vHandleHubspotRateLimitTest v100% <8• Thu 14 May 17:35:18* :QProject v+.•Х:-© AutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.phpwmieauonv PipedrivePlanhatService.phpAutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php> C OpportunitySyncStrategy© ImportActivityTypes.phpT WriteCrmTrait.php© DecorateActivity.php© Salesforce/Service.php xT LogActivityTrait.php> D ProspectSearchStrategy© Pipedrive/Service.phpClose/Service.phpCopper/Service.phpBullhornService.phpPlainTextDecorateActivity.php© ApiFields.php© Client.phpT ActivityPlaybookTrait.phpCrmHelperRepository.php© AccountController.php=.env.staging=.env18© FieldDefinitions.php© DetachActivityObject.php© PipedriveApiClient.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.php© PipedriveApiException.php© HubspotPaginationService.php© HandleHubspotRateLimit.php© Service.php80classService extends BaseService implements© TokenStorage.php135A11 A130 X3 X21 ^private OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;v D Salesforce136> • Fields137 ₫public function__construct(> OpportunityMatcher138Client Sclient,> D OpportunitySyncStrategy139PayloadBuilder $payloadBuilder,> D ProspectSearchStrategy140 đprotected Dispatcher $eventDispatcher,v D ServiceTraits141private readonly CountriesMap $countriesMap,T BatchSyncTrait.php142private readonly ProspectPhotoPathService $prospectPhotoPathService,( FollowupActivityTrait.php143) f...3T LogActivityTrait.php156+ RecordManipulationsTrait.p157 >public function getDisplayName(): string{...}T SyncFieldsTrait.php161© Client.php162 ›public function getJobDelay(): intf.© DecorateActivity.php166G DeleteObjectsTrait.php167 đ >protected function get0AuthAccount(User $user): ?SocialAccount{...}© FieldDefinitions.php171© PayloadBuilder.php172 € >public function verifyTaskExists(Activity $activity): bool{...}© Profile.php207© QueryBuilder.php208 0 >public function query(string $queryToRun, array $parameters = []): QueryIterator{...}© QueryHandler.php213© Querylterator.php214/*===Organization Information ======© QueryResults.php215© Service.php216/**© SyncBatchRedisService.php217* Get a list of all the API Versions for the instance.Traits218*© BaseClient.php219* Athrows CrmExceptionWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17):=custom.log= laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]DGo jiminny084143 ×4 л215•m_layout_id =2162,1661,66799,66id = 33;— 220221222$223224225226227228229- 230— 231- 232- 233 V234id THEN •(ownhubspot':= 11512582;I I11W Windsurf Teams100:25UTF-8Ca 4 spaces...
|
45408
|
NULL
|
NULL
|
NULL
|
|
45410
|
1626
|
28
|
2026-05-14T14:35:18.551547+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769318551_m2.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
ActivityFilesLaterJiminny... ~@ jiminny-x-integrat ActivityFilesLaterJiminny... ~@ jiminny-x-integrati& platform-inner-team© Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity_lab# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the people of iimi..• Direct messages8. A...• A3 62P. Galya Dimitrova Mvasil VasilevA. Stefka Stoyanova%: Todor Stamatovf. Mario GeorgievP. Nikolay Ivanov2o James Graham "2. Stoyan Tanev. Steliyan Georgiev. Petko Kashinski*. Lukas Kovali...a: Apps® ToastS lira Gloud6 Huddle with Aneliya Angelova& R. Aneliya Angelova •• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някьде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova @ 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preview insackt[HubSpot] Optimise CRM rematctC OpenReady for QA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira+ SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle LIVE 4:06 PMAneliva Angelova is here toolAneliya Angelova 4:44 PM11512582Lukas Kovalik O 5:16 PMcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-Type: application/json'\--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIґ7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pm~ (Q SearchDate ModifiedYesterdav at 13.3Yesterday at 13:37Yesterday at 13.30Yesterday at 13:35resterday at 15.34Yesterdav at 13:34Yesterday at 13:33Yesterday at 13.32Yesterday at 13:32resterday at 15.3Yesterdav at 13:30Yesterday at 13:30Yesterday at 13:29Yesterday at 13:29Yesterday at 13:28resterday at 13-21Yesterdav at 13:27Yesterday at 13:26Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:22Yesterday at 13:22resterady al 15.clYesterday at 13:20Yesterday at 13.19Yecterdav at 12:10Yesterday at 13:18Yesterday at 13.10Yesterdav at 13:17Yesterday at 13:17Yesterday at 13:16Yacterdav at 12:15Yesterday at 13.14Yecterdav at 12:14Yesterday at 13:13Yesterday at 13:12Yesterday at 13:12Yesterday at 13.11Yecterdav at 12:10Yesterday at 13:10Yesterdav at 13:0962 K:MPEG-4 movie71 KBMPEG-4 movie23 KBMPEG-4 movieMPEG-4 movie13 KE16 KB17 KBMPE0"4 movie29 K:6 KBMPEG-4 movie12 KBMP2G-4 movieMPEG-4 movie7 KBI8 KB37 KBMPEG"4 movie10 KВIMPEG-4 movie7 KBMPEG-4 movie8 KB9 KB1MPEG-4 movie8 KBMPEG-4 movie72 KB14 KBMPEG-4 movie13 KEMPEG-4 movie9 KB MPEG-4 movie18 KB12 KB|MPEG-4 movie10 KBMPEG-4 movie16 KB6KBMPEG-4 movie6KEMPEG-4 movie12 KBMPEG-4 movie23 KBMPEG-4 movie8 K:MPEG-4 movie6 KBMPEG-4 movie11 KBMPEG-4 movie11 KPMPEG-4 movie20 KB34 KB10 K:MPEG-4 movie7KBMPEG-4 movie5 KB11 KBMPEG-4 movieD6KRMDEG-A movia111 KBMPEG-4 movie102 KBMPEG-4 movie88 KBMPEG-A movie59 KB MPEG-4 movie98 KB97 KBGAKPMPEG-4 movieMPEG-4 movie44 KB93 KBMPEG-4 movie78 KPMDEG-A movid50 KBMPEG-4 movie58 KB17 KBMPEG-4 movieAl Notes: OffLeave• 0FavouritesE jiminny© Recents* ApplicationsiCloudiCloud Drive228 Sync folderLocationsQ DXP4800PLUS-B5FA@ Network|• CRMI• Orange• Red• Yellow• Green• Purple•) All lags..lohlDownloadsNameLoom.pkgAlfred copv.alfredoreterencesB KeychronAssist-1.0.2 (1).dmgA Keychron Assist-1.0.2.dmgmazanoke-images-yWJo.zio• Photos-3-001.zipD Transcript.pdf→mage U.loge1 Orioninstaller.dmaimage.jpg- image (2).1pc• ПО-22221726037035-004-001_ORGES.pdf• %D0%9F%D0%9E-22221726037035-004-001_archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterences05012026_0000000026574472_ SWIFT_OB70501260015890.pdf27022026_0000000026574472_SWIFT_OB72702260049200.pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001B reporti) xm=pdt.odipdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfKoválik Family Tree.gedbitwarden export 20251031122528.isonKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst_aid_notes_complete.docxrenortl2).esvconfig.ymlIteration run Search HS.postman_collection.jsonm licence bettertouchtoalMariusHosting Config.json1ooks-891a6503-bbb7-4b2b-9c3.csv•Alfredmazanoke-images-YWJ6ана Ковалик.jpg•искане даниел Ковалик..pa• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик.jpg• Dhotac 2.001Q SearchKind00,4 MDinstdlle..dckage55.9 MBAlfred...ferences10,1 MBDisk Image10,1 MBDisk ImageIL MBLiP archive6,6 MBZIP archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDF Document140 KbZIP archive148 KВ148 KB122 KB111 KB94 KBZIP archivelZIP archiveXML documentAlfred...ferencesPDF Document92 KBPDr DocumentK:91 KB91 KB30 KB29 KBPDF DocumentXML documentPDF Document28 KBPDF Document28 KR28 KB27 KB14 KB11 KB6 KB6KBPDE DocumentDocumentJSONICSV DocumentZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBYAML document1 KBcSV Document928 byteshttlicence183 bytesZero butesJSONAlfred.. ferencesZero bytesFolde1,9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.48 GB availabld• Inu 14 May 1/.30.10Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3713 Feb 2026 at 11:54Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nov 2025 at 17:596 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0418 Mar 2026 at 11:5510 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:3630 Jan 2026 at 12:3616 Oct 2025 aт 16:0123 Apr 2026 at 13:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
NULL
|
7850357533355687519
|
NULL
|
click
|
ocr
|
NULL
|
ActivityFilesLaterJiminny... ~@ jiminny-x-integrat ActivityFilesLaterJiminny... ~@ jiminny-x-integrati& platform-inner-team© Channels# ai-chapter# alertsi backend# bugscontusion-cllnia# curiosity_lab# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the people of iimi..• Direct messages8. A...• A3 62P. Galya Dimitrova Mvasil VasilevA. Stefka Stoyanova%: Todor Stamatovf. Mario GeorgievP. Nikolay Ivanov2o James Graham "2. Stoyan Tanev. Steliyan Georgiev. Petko Kashinski*. Lukas Kovali...a: Apps® ToastS lira Gloud6 Huddle with Aneliya Angelova& R. Aneliya Angelova •• Messagest Add canvasur FilesAneliya Angelova 2:30 PMЛукаш, само при Сейлсфорс и Хубспот се синкваха активити типовете, когато се направи плейбук,нали?пои останалите скМі тоябва оъчно ла се въвелат.Lukas Kovalik 2:47 PMтряова да се за всички, някьде не се ли попьлвапри зохо май беше hardcoded но май и там си връщаха две категорииAneliya Angelova @ 4:00 PMЛукаш имаш ли време да се чуем за тестването на https://jiminny.atlassian.net/browse/JY-20725X Preview insackt[HubSpot] Optimise CRM rematctC OpenReady for QA- MediumAA Aneliya AngelovaAs of today at 4:00 PM RetreshOpen in Jira+ SummariseLukas Kovalik 4:02 PMzean-iurwiniareelYou joined the huddle LIVE 4:06 PMAneliva Angelova is here toolAneliya Angelova 4:44 PM11512582Lukas Kovalik O 5:16 PMcurl --location "httns:ani.hubani.com/crm/v3/obiects/contacts/search--header 'Content-Type: application/json'\--header "Authorization: BearerC|Kvma?iMx7OINOM8kOFwrAowAcAkUAhIR24?A05D?xiVrIYMILuP6SA01Kwcmhr=n.v2 hytorDniMvDalYck-CMalovolNOM9kOFwrAUAcrkcaws-ThwRARIRAOF-ATFSAOFRAO-ROIEBAQUBEggBAQEBAYICFIґ7Au8O2Nwal9YW2CbCCxYvwngmSgNldTFSAFoAYABoAHAAeAA'\--data "{ "limit", 13'Message Aneliva Angelova+ AaIđ 0Pm~ (Q SearchDate ModifiedYesterdav at 13.3Yesterday at 13:37Yesterday at 13.30Yesterday at 13:35resterday at 15.34Yesterdav at 13:34Yesterday at 13:33Yesterday at 13.32Yesterday at 13:32resterday at 15.3Yesterdav at 13:30Yesterday at 13:30Yesterday at 13:29Yesterday at 13:29Yesterday at 13:28resterday at 13-21Yesterdav at 13:27Yesterday at 13:26Yesterday at 13:26Yesterday at 13:25Yesterday at 13.24Yesterdav at 13:22Yesterday at 13:22resterady al 15.clYesterday at 13:20Yesterday at 13.19Yecterdav at 12:10Yesterday at 13:18Yesterday at 13.10Yesterdav at 13:17Yesterday at 13:17Yesterday at 13:16Yacterdav at 12:15Yesterday at 13.14Yecterdav at 12:14Yesterday at 13:13Yesterday at 13:12Yesterday at 13:12Yesterday at 13.11Yecterdav at 12:10Yesterday at 13:10Yesterdav at 13:0962 K:MPEG-4 movie71 KBMPEG-4 movie23 KBMPEG-4 movieMPEG-4 movie13 KE16 KB17 KBMPE0"4 movie29 K:6 KBMPEG-4 movie12 KBMP2G-4 movieMPEG-4 movie7 KBI8 KB37 KBMPEG"4 movie10 KВIMPEG-4 movie7 KBMPEG-4 movie8 KB9 KB1MPEG-4 movie8 KBMPEG-4 movie72 KB14 KBMPEG-4 movie13 KEMPEG-4 movie9 KB MPEG-4 movie18 KB12 KB|MPEG-4 movie10 KBMPEG-4 movie16 KB6KBMPEG-4 movie6KEMPEG-4 movie12 KBMPEG-4 movie23 KBMPEG-4 movie8 K:MPEG-4 movie6 KBMPEG-4 movie11 KBMPEG-4 movie11 KPMPEG-4 movie20 KB34 KB10 K:MPEG-4 movie7KBMPEG-4 movie5 KB11 KBMPEG-4 movieD6KRMDEG-A movia111 KBMPEG-4 movie102 KBMPEG-4 movie88 KBMPEG-A movie59 KB MPEG-4 movie98 KB97 KBGAKPMPEG-4 movieMPEG-4 movie44 KB93 KBMPEG-4 movie78 KPMDEG-A movid50 KBMPEG-4 movie58 KB17 KBMPEG-4 movieAl Notes: OffLeave• 0FavouritesE jiminny© Recents* ApplicationsiCloudiCloud Drive228 Sync folderLocationsQ DXP4800PLUS-B5FA@ Network|• CRMI• Orange• Red• Yellow• Green• Purple•) All lags..lohlDownloadsNameLoom.pkgAlfred copv.alfredoreterencesB KeychronAssist-1.0.2 (1).dmgA Keychron Assist-1.0.2.dmgmazanoke-images-yWJo.zio• Photos-3-001.zipD Transcript.pdf→mage U.loge1 Orioninstaller.dmaimage.jpg- image (2).1pc• ПО-22221726037035-004-001_ORGES.pdf• %D0%9F%D0%9E-22221726037035-004-001_archive.zipПO-22221726037035-004-001_archive (1).zip• repon 4).XmAltred copy2.altredoreterences05012026_0000000026574472_ SWIFT_OB70501260015890.pdf27022026_0000000026574472_SWIFT_OB72702260049200.pdf= 03042026 [CREDIT_CARD] SWIFT [CREDIT_CARD].001B reporti) xm=pdt.odipdf-1.pdfD pdf-2.pdf-pdf-5.pd1= ndf-1 ndipdf-3.pdfKoválik Family Tree.gedbitwarden export 20251031122528.isonKoválik Family Tree.zip*macOS Storage_Cleanup.mdal favicon icofirst_aid_notes_complete.docxrenortl2).esvconfig.ymlIteration run Search HS.postman_collection.jsonm licence bettertouchtoalMariusHosting Config.json1ooks-891a6503-bbb7-4b2b-9c3.csv•Alfredmazanoke-images-YWJ6ана Ковалик.jpg•искане даниел Ковалик..pa• Фактура Март Даниел Ковалик.jрс• Фактура Април Даниел Ковалик.jpg• Dhotac 2.001Q SearchKind00,4 MDinstdlle..dckage55.9 MBAlfred...ferences10,1 MBDisk Image10,1 MBDisk ImageIL MBLiP archive6,6 MBZIP archive2,5 MB PDF Document2,5 MBJreo lmage2.2 MBDisk Image2,2 MB PDF Document2 MBJPEG image1,9 MBJPEG imaqe192 KBPDF Document140 KbZIP archive148 KВ148 KB122 KB111 KB94 KBZIP archivelZIP archiveXML documentAlfred...ferencesPDF Document92 KBPDr DocumentK:91 KB91 KB30 KB29 KBPDF DocumentXML documentPDF Document28 KBPDF Document28 KR28 KB27 KB14 KB11 KB6 KB6KBPDE DocumentDocumentJSONICSV DocumentZIP archiveMarkdo…..ument5KRWindo...n image4 KB3 KBword ..cumentCSV Document2 KBYAML document1 KBcSV Document928 byteshttlicence183 bytesZero butesJSONAlfred.. ferencesZero bytesFolde1,9 MB1,8 MB17 MB1,7 MBColdorJPEG imageJPEG ImageIPEG imadeJPEG imageColdo1 of 58 selected, 8.48 GB availabld• Inu 14 May 1/.30.10Date AddedIs Mdl ZUzo dl 19:4530 Jan 2026 at 12:3617 Mar 2026 at 20:2717 Mar 2026 at 20:26Z3 Aor 2020 al 13:0229 Jan 2026 at 15:2019 Dec 2025 at 10:1619 Dec 2025 at 12:238 Aor 2026 at 20:3519 Dec 2025 at 10:2919 Dec 2025 at 12:1819 Dec 2025 at 12:4026 Mar 2026 at 11:2410 May 2026 at 13:5326 Mar 2026 at 11:2426 Mar 2026 at 11:2426 Mar 2026 at 11:2310 May 2026 at 14:3730 Jan 2026 at 12:3713 Feb 2026 at 11:54Z3 Apr 2026 at 13.0823 Aor 2026 at 13:0810 May 2026 at 13:5410 May 2026 at 14:3710 May 2026 at 13:4910 May 2026 at 13:5010 May 2026 at 13:5110 Mav 2026 at 12:51110 May 2026 at 13:5019 Dec 2025 at 11:3431 Oct 2025 at 12:2525 Nov 2025 at 17:596 Mar 2026 at 11.2224 Anr 2026 at 16:5220 Oct 2025 at 11:0218 Mar 2026 at 15:29• Mav 2026 at 11:09129 Oct 2025 at 19:329 May 2026 at 10:0418 Mar 2026 at 11:5510 May 2026 at 13:5712 Jun 2025 at 19:0430 Jan 2026 at 12:3630 Jan 2026 at 12:3616 Oct 2025 aт 16:0123 Apr 2026 at 13:0223 Apr 2026 at 13:0223 Apr 2026 at 13:0222 Anr 2026 at 12:0223 Apr 2026 at 13:0229 Jan 2026 at 15:20...
|
45407
|
NULL
|
NULL
|
NULL
|
|
45411
|
1625
|
49
|
2026-05-14T14:35:20.839337+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769320839_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.054166667,"top":0.027777778,"width":0.08055556,"height":0.035555556},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.13472222,"top":0.027777778,"width":0.25555557,"height":0.035555556},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.6326389,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.6645833,"top":0.027777778,"width":0.15902779,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.82361114,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.8472222,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.87083334,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9291667,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9527778,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9763889,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"bounds":{"left":0.78055555,"top":0.3122222,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.80138886,"top":0.3122222,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"}]...
|
-117723898423684695
|
-7123497198205957173
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl19100% C•FV faVsco.jsv* #12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTest* :8• Thu 14 May 17:35:20QProjectv© UserAutomatedReportsController.php:= custom.logAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php( SyncCrmEntitiesTrait.phpPlanhatService.phpT SyncFieldsTrait.php© ImportActivityTypes.phpT WriteCrmTrait.php> O Utils© Pipedrive/Service.php© AutomatedReportResult.php€ WriteCrmTrait.php xClose/Service.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.php© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.phpCopper/Service.phpBullhornService.php© PlainTextDecorateActivity.php> D Webhook© ActivityPlaybookTrait.php© CrmHelperRepository.php© AccountController.php=.env.staging=.env18© BatchSyncCollector.php© BatchSyncRedisService.php© DetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.php© Client.php© HandleHubspotRateLimit.php© ClosedDealStagesService.phpC DealFieldsService.php© HubspotPaginationService.phptrait WriteCrmTraitprivate function buildConferencePayload(Activity $activity): Activity© DecorateActivity.php© FieldDefinitions.php© FieldTypeConverter.php• HubspotClientinterface.php© HubspotTokenManager.php© PayloadBuilder.php© RemoteCrmObjectManipulator.© ResponseNormalize.php© Service.php© SyncFieldAction.php© SyncRelatedActivityManager.p© WebhookSyncBatchProcessor.v D IntegrationApp> OJ Accessors> Api.› C Config> ODTO ×6 л// Only if they are creating a fresh engagement should we update the times, otherwise it will causeif (! $activity->hasCrmProviderId()) {if (Sactivity->hasActualStartTime() && Sactivity->hasActualEndTime()) €Smetadata['startTime'] = Sdecorator->formatStartTime(Sactivity);$metadata['endTime'] = $decorator->formatEndTime(Sactivity);$metadata['title'] = $decorator-›generateTitle($activity);// For now, this is just a "meeting0utcome" field for somecustomers.Smetadata = array_merge($metadata, $this->fetchCustomFieldData(Sactivity, Field::0BJECT_TASK)):return $this->upsertActivity(Sactivity, $engagement, Smetadata);> O Filters2 usages> D Jobs.D ProspectSearchStrategy• Service Traits© DataClient.php© DecorateActivity.php© LocalSearch.php90106107108109110private function fetchCustomFieldData(Activity Sactivity, string $objectType): array{...}/*** @throws HubspotException* Othrows BadRequest*/• LocalSearchinterface.php3 usagesA DamntaCaarch nhn111private function upsertActivitv(Activity Sactivity. array $enqagement, array Smetadata): Activitví...}Workspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]DGo jiminny084143 ×4 л215•m_layout_id =2162,1661,66799,66— 217— 218|219.id = 33;220221222223224id THEN •(own225226227228229230hubspot':=231_232233V= 11512582;234W Windsurf Teams81:54UTF-8P. 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
45452
|
1627
|
4
|
2026-05-14T14:37:39.552341+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769459552_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl100% <47FV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTest8• Thu 14 May 17:37:39* :QProject vAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.php:• JobsPlanhatService.php© AutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.phpProspectSearchStrategyD ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.php X© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php X© DataClient.php© Playbook.php© PipedrivService.php© DecorateActivity.php© PlainTextDecorateActivity.php~/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits/WriteCrmTrait.phpmowoaniController.php=.env.staging18© LocalSearch.php• LocalSearchlnterface.phpE.envDetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.phpC RemoteSearch.php© Service.php© HubspotPaginationService.php© HandleHubspotRateLimit.php15.04.26 Vasilev24trait LogActivityTraitv C ListenersA6 X1л23.04.26 Vasilev85private function buildSubjectPayload(Activity $activity, Playbook $playbook): array© ConvertLeadActivities.php21.04.26 Vasilev90* in the CRM, then we set the activity type in the payload.© PurgeLookupCache.php21.04.26 Vasilev91* Some activity types may be locally created. They shouldn't be sent to Salesfol 216› D Metadata21.04.26 Vasilev92*> C Migration21.04.26 Vasilevv 0 Pipedrive93* The resulting payload attribute will look like:21.04.26Vasilev94Type=> 'Cold Call'> D OpportunitySyncStrategy21.04.26 Vasilev95*/> ProspectSearchStrategy23.04.26 Vasilev96return [© ApiFields.php23.04.26 Vasilev97/** ActivityFieldID => ActivityType */© Client.php15.04.26 Vasilev98$playbook->getActivityField()->getCrmProviderId() => $activity->getCategoryC.223© FieldDefinitions.php20.04.26 Vasilev99© PipedriveApiClient.php'Subject' => Sthis->getActivityDecorator()->generateTitle(Sactivity),15.04.26 Vasilev1001:• PipedriveApiException.php23.04.26 Vasilev101© Service.php15.04.26 Vasilev102© TokenStorage.php23.04.26 Vasilev103/**v D Salesforce23.04.26 Vasilev104* Building of the title seems very inconsistent• Fields23.04.26 Vasilev105* Subject here will be something like:• OpportunityMatcher23.04.26 Vasilev106*"Local activity type: Activity Title"• OpportunitySyncStrategy23.04.26 Vasilev107*/D ProspectSearchStrategy23.04.26 Vasilev108v • ServiceTraits$subject = Sactivity->getCategory(->getNameO:23.04.26 Vasilev109if (Sactivity->getTitle() |== null) {T BatchSyncTrait.php23.04.26 Vasilev110+ FollowupActivityTrait.php$subject .= ': ' . $activity->getTitle();15.04.26 Vasilev111T LogActivityTrait.php15.04.26 Vasilev112+ RecordManipulationsTrait./23.04.26 Vasilev113return ['Subject' => $subject];T SyncFieldsTrait.php23.04.26 Vasilev114@ Client nhnWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)= custom.log=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]DGojiminny v084143 ×4 л215•m_layout_id =2,1661,66799,66217218219id = 33;220221222224id THEN '(own225226227228229230hubspot':-231232233V= 11512582;234W Windsurf Teams98:92 (11 chars)UTF-8Ca 4 spaces...
|
NULL
|
-6973900021411041732
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormFileEditViewNavigateCodeLaravelRefactorRun PhpStormFileEditViewNavigateCodeLaravelRefactorRunToolsGitWindowHelplhl100% <47FV faVsco.jsv#12066 on JY-20725-handle-HS-search-rate-limit k vHandleHubspotRateLimitTest8• Thu 14 May 17:37:39* :QProject vAutomatedReportGenerated.phpTrackAutomatedReportGeneratedEvent.php© UserAutomatedReportsController.php:• JobsPlanhatService.php© AutomatedReportResult.phpSendReportJob.phpDeleteCrmEntityTrait.phpDeleteAccountJob.phpProspectSearchStrategyD ServiceTraits© ImportActivityTypes.phpT WriteCrmTrait.php X© DecorateActivity.php© Salesforce/Service.phpT LogActivityTrait.php X© DataClient.php© Playbook.php© PipedrivService.php© DecorateActivity.php© PlainTextDecorateActivity.php~/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits/WriteCrmTrait.phpmowoaniController.php=.env.staging18© LocalSearch.php• LocalSearchlnterface.phpE.envDetachActivityObject.phpRematchActivityOnCrmObjectDetach.phpMatchActivityCrmData.phpClient.phpC RemoteSearch.php© Service.php© HubspotPaginationService.php© HandleHubspotRateLimit.php15.04.26 Vasilev24trait LogActivityTraitv C ListenersA6 X1л23.04.26 Vasilev85private function buildSubjectPayload(Activity $activity, Playbook $playbook): array© ConvertLeadActivities.php21.04.26 Vasilev90* in the CRM, then we set the activity type in the payload.© PurgeLookupCache.php21.04.26 Vasilev91* Some activity types may be locally created. They shouldn't be sent to Salesfol 216› D Metadata21.04.26 Vasilev92*> C Migration21.04.26 Vasilevv 0 Pipedrive93* The resulting payload attribute will look like:21.04.26Vasilev94Type=> 'Cold Call'> D OpportunitySyncStrategy21.04.26 Vasilev95*/> ProspectSearchStrategy23.04.26 Vasilev96return [© ApiFields.php23.04.26 Vasilev97/** ActivityFieldID => ActivityType */© Client.php15.04.26 Vasilev98$playbook->getActivityField()->getCrmProviderId() => $activity->getCategoryC.223© FieldDefinitions.php20.04.26 Vasilev99© PipedriveApiClient.php'Subject' => Sthis->getActivityDecorator()->generateTitle(Sactivity),15.04.26 Vasilev1001:• PipedriveApiException.php23.04.26 Vasilev101© Service.php15.04.26 Vasilev102© TokenStorage.php23.04.26 Vasilev103/**v D Salesforce23.04.26 Vasilev104* Building of the title seems very inconsistent• Fields23.04.26 Vasilev105* Subject here will be something like:• OpportunityMatcher23.04.26 Vasilev106*"Local activity type: Activity Title"• OpportunitySyncStrategy23.04.26 Vasilev107*/D ProspectSearchStrategy23.04.26 Vasilev108v • ServiceTraits$subject = Sactivity->getCategory(->getNameO:23.04.26 Vasilev109if (Sactivity->getTitle() |== null) {T BatchSyncTrait.php23.04.26 Vasilev110+ FollowupActivityTrait.php$subject .= ': ' . $activity->getTitle();15.04.26 Vasilev111T LogActivityTrait.php15.04.26 Vasilev112+ RecordManipulationsTrait./23.04.26 Vasilev113return ['Subject' => $subject];T SyncFieldsTrait.php23.04.26 Vasilev114@ Client nhnWorkspace associated with branch 'JY-20725-handle-HS-search-rate-limit' has been restored // Rollback // Configure... (today 16:17)= custom.log=laravel.logSF [jiminny@localhost]A HS_local [jiminny@localho:W4 console [QAI PROD] X4 console [PROD]A console (EU]DGojiminny v084143 ×4 л215•m_layout_id =2,1661,66799,66217218219id = 33;220221222224id THEN '(own225226227228229230hubspot':-231232233V= 11512582;234W Windsurf Teams98:92 (11 chars)UTF-8Ca 4 spaces...
|
45450
|
NULL
|
NULL
|
NULL
|
|
45453
|
1627
|
5
|
2026-05-14T14:37:41.144897+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778769461144_m1.jpg...
|
PhpStorm
|
faVsco.js – WriteCrmTrait.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.054166667,"top":0.027777778,"width":0.08055556,"height":0.035555556},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12066 on JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.13472222,"top":0.027777778,"width":0.25555557,"height":0.035555556},"on_screen":true,"help_text":"Pull request #12066 exists for current branch JY-20725-handle-HS-search-rate-limit, but local branch is out of sync with remote","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.6326389,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.6645833,"top":0.027777778,"width":0.15902779,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.82361114,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","depth":6,"bounds":{"left":0.8472222,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.87083334,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9291667,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9527778,"top":0.027777778,"width":0.023611112,"height":0.035555556},"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.9763889,"top":0.027777778,"width":0.023611112,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"bounds":{"left":0.78055555,"top":0.3122222,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.80138886,"top":0.3122222,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.8215278,"top":0.31,"width":0.015277778,"height":0.025555555},"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.8368056,"top":0.31,"width":0.014583333,"height":0.025555555},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Repositories\\Crm\\FieldDataRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DecorateActivity;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationCategoryV4;\nuse Jiminny\\Services\\Crm\\Hubspot\\Utils\\AssociationTypeIdV4;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Sentry;\n\ntrait WriteCrmTrait\n{\n private const string ACTION_CREATE = 'create';\n private const string ACTION_UPDATE = 'update';\n private const string ACTION_DELETE = 'delete';\n\n /**\n * @inheritdoc\n */\n public function saveActivity(Activity $activity): Activity\n {\n switch ($activity->getCrmType()) {\n case Activity::TYPE_CONFERENCE:\n $activity = $this->buildConferencePayload($activity);\n\n break;\n case Activity::TYPE_SOFTPHONE:\n case Activity::TYPE_SOFTPHONE_INBOUND:\n $activity = $this->buildSoftphonePayload($activity);\n\n break;\n\n case Activity::TYPE_SMS_INBOUND:\n case Activity::TYPE_SMS_OUTBOUND:\n $activity = $this->buildTextMessagePayload($activity);\n\n break;\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildConferencePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatStartTime($activity),\n 'type' => 'MEETING',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'meetingOutcome' => $decorator->convertMeetingOutcome($activity),\n 'internalMeetingNotes' => $decorator->generateDescription($activity),\n ];\n\n // Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.\n if (! $activity->hasCrmProviderId()) {\n if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {\n $metadata['startTime'] = $decorator->formatStartTime($activity);\n $metadata['endTime'] = $decorator->formatEndTime($activity);\n }\n $metadata['title'] = $decorator->generateTitle($activity);\n }\n\n // For now, this is just a \"meetingOutcome\" field for some customers.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function fetchCustomFieldData(Activity $activity, string $objectType): array\n {\n $payload = [];\n\n $fieldDataRepository = app(FieldDataRepository::class);\n $fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);\n\n foreach ($fieldData as $data) {\n // Add the field and value to the payload.\n $payload += [\n $data->getField()->getCrmProviderId() => $data->getValue(),\n ];\n }\n\n return $payload;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity\n {\n if (! $activity->hasCrmProviderId()) {\n $this->createActivity($activity, $engagement, $metadata);\n\n return $activity;\n }\n\n try {\n $oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());\n } catch (HubspotException $e) {\n $this->logger->warning('[HubSpot] Engagement not found', [\n 'activity_id' => $activity->getUuid(),\n 'crm_provider_id' => $activity->getCrmProviderId(),\n 'exception' => $e->getMessage(),\n ]);\n\n // potentially here we can log new activity if it is not found\n\n return $activity;\n }\n\n $type = $oldEngagement['engagement']['type'] ?? null;\n $bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';\n\n $decorator = $this->getDecorateActivity();\n\n if ($type !== 'TASK') {\n $oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldDescription\n );\n\n $oldAssociations = $oldEngagement['associations'] ?? [];\n $this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);\n } else {\n $oldTaskBody = $oldEngagement['metadata']['body'] ?? '';\n $metadata[$bodyAttribute] = $decorator->mergeDescriptions(\n $metadata[$bodyAttribute],\n $oldTaskBody\n );\n $this->updateTask($activity, $engagement, $metadata);\n }\n\n return $activity;\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateNonTaskActivity(\n Activity $activity,\n array $engagement,\n array $metadata,\n array $oldAssociations\n ): void {\n if ($activity->isTypeConference()) {\n $this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);\n } else {\n $this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n }\n\n $this->updateActivityAssociations($activity, $oldAssociations);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n }\n\n private function updateActivityAssociations(Activity $activity, array $oldAssociations): void\n {\n $currentAssociations = $this->getActivityAssociations($activity);\n $associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);\n\n // No changes - exit\n if (empty($associationChanges)) {\n return;\n }\n\n $this->logger->info('[HubSpot] Updating associations', [\n 'team' => $this->team->getUuid(),\n 'activity' => $activity->getUuid(),\n 'associationChanges' => $associationChanges,\n ]);\n\n $this->updateAssociations($activity, $associationChanges);\n }\n\n private function getAssociationDifferences(\n Activity $activity,\n array $oldAssociations,\n array $currentAssociations\n ): array {\n $objects = ['contactIds', 'companyIds', 'dealIds'];\n $changes = [];\n\n foreach ($objects as $object) {\n $oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);\n $currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);\n\n $added = array_diff($currentIds, $oldIds);\n $removed = array_diff($oldIds, $currentIds);\n\n $type = match ($object) {\n 'contactIds' => 'contact',\n 'companyIds' => 'company',\n 'dealIds' => 'deal',\n };\n\n $shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);\n\n if (! empty($removed) && ! $shouldIncludeRemoved) {\n $this->logger->info('[HubSpot] Skipping association removal', [\n 'team' => $this->team->getUuid(),\n 'type' => $type,\n 'oldIds' => $oldIds,\n 'currentIds' => $currentIds,\n 'skippedRemovals' => array_values($removed),\n ]);\n }\n\n if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {\n $changes[$type] = [\n 'added' => array_values($added),\n 'removed' => array_values($removed),\n ];\n }\n }\n\n return $changes;\n }\n\n /**\n * Determines whether removed associations should be synced to HubSpot.\n *\n * For contacts and companies, we always sync removals (original behavior).\n * For deals, we only sync removals when:\n * - There are new deals being added (replacement scenario), OR\n * - The activity has an account associated (indicating intentional association management)\n *\n * This prevents accidentally removing deal associations that exist in HubSpot\n * but not in Jiminny's database (e.g., deals manually added by users in HubSpot).\n */\n private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool\n {\n // Contacts and companies: always sync removals\n if ($type !== 'deal') {\n return true;\n }\n\n // Deals: sync removals if we're adding new deals (replacement)\n if (! empty($added)) {\n return true;\n }\n\n // Deals: sync removals only if activity has an account (intentional management)\n return $activity->hasAccount();\n }\n\n private function normalizeIds(array $ids): array\n {\n $stringIds = array_map('strval', $ids);\n $uniqueIds = array_unique($stringIds);\n sort($uniqueIds);\n\n return $uniqueIds;\n }\n\n private function updateAssociations(Activity $activity, array $associationChanges): void\n {\n $objectType = $activity->isTypeConference() ? 'meeting' : 'call';\n\n foreach ($associationChanges as $type => $changes) {\n if (! empty($changes['added'])) {\n $associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);\n $payload = $this->payloadBuilder->buildAddAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['added'],\n $associationType\n );\n $this->getClient()->addAssociations($objectType, $type, $payload);\n }\n\n if (! empty($changes['removed'])) {\n $payload = $this->payloadBuilder->buildRemoveAssociationPayload(\n $activity->getCrmProviderId(),\n $changes['removed']\n );\n $this->getClient()->removeAssociations($objectType, $type, $payload);\n }\n }\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response\n {\n // If already set by us/client, do not override it\n if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {\n $this->logger->info('[HubSpot] Meeting activity type already set', [\n 'meetingId' => $meetingId,\n 'activityType' => $engagement['activityType'],\n ]);\n\n unset($engagement['activityType']);\n }\n\n $payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);\n\n return $this->getClient()->updateMeeting($meetingId, $payload);\n }\n\n private function hasEngagementActivityType(string $engagementId): bool\n {\n try {\n $engagementData = $this->getClient()->getEngagementData($engagementId);\n if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {\n return true;\n }\n\n return false;\n } catch (\\Exception $exception) {\n Sentry::captureException($exception);\n\n $this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [\n 'engagementId' => $engagementId,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n return true;\n }\n\n private function updateTask(Activity $activity, array $engagement, array $metadata): void\n {\n $this->getClient()->deleteEngagement($activity->getCrmProviderId());\n\n $this->logCrmEngagementManipulation(\n self::ACTION_DELETE,\n ['crmId' => $activity->getCrmProviderId()],\n );\n\n $this->createActivity($activity, $engagement, $metadata);\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createActivity(Activity $activity, array $engagement, array $metadata): void\n {\n $associations = $this->getActivityAssociations($activity);\n $associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);\n\n if ($activity->isTypeConference()) {\n try {\n $hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);\n $activityId = $hsEngagement->data->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n } else {\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n $activityId = $hsEngagement->data->engagement->id;\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $activity->crm_provider_id = $activityId;\n $activity->save();\n } catch (BadRequest $exception) {\n Sentry::captureException($exception);\n\n $this->logger->warning('[HubSpot] Error while creating engagements', [\n 'activity' => $activity->getUuid(),\n 'message' => $exception->getMessage(),\n 'engagement' => $engagement,\n 'associations' => $associations,\n 'metadata' => $metadata,\n ]);\n }\n }\n }\n\n private function convertActivityMeetingAssociationsV4(Activity $activity): array\n {\n $associations = [];\n\n $contactIds = $this->getParticipantsIds($activity);\n\n foreach ($contactIds as $contactId) {\n $associations[] = $this->buildAssociation(\n (int) $contactId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]\n );\n }\n\n if ($activity->hasAccount()) {\n $crmProviderId = $activity->getAccount()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]\n );\n }\n }\n\n if ($activity->hasOpportunity()) {\n $crmProviderId = $activity->getOpportunity()?->getCrmProviderId();\n if ($crmProviderId) {\n $associations[] = $this->buildAssociation(\n (int) $crmProviderId,\n [AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]\n );\n }\n }\n\n return $associations;\n }\n\n private function buildAssociation(int $id, array $categoryToTypeIds): array\n {\n $types = [];\n\n foreach ($categoryToTypeIds as $category => $typeId) {\n $types[] = $this->buildAssociationType($category, $typeId);\n }\n\n return [\n 'to' => ['id' => (string) $id],\n 'types' => $types,\n ];\n }\n\n private function buildAssociationType(string $category, int $typeId): array\n {\n return [\n 'associationCategory' => $category,\n 'associationTypeId' => $typeId,\n ];\n }\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function createMeetingV3(array $engagement, array $associations, array $metadata): Response\n {\n $payload = $this->payloadBuilder->getV3MeetingPayload(\n engagement: $engagement,\n metadata: $metadata,\n associations: $associations\n );\n\n return $this->getClient()->createMeeting($payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n private function buildSoftphonePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'CALL',\n ];\n\n if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {\n $engagement['activityType'] = $activity->getActivityType()->getName();\n }\n\n $metadata = [\n 'toNumber' => $decorator->formatCallToParticipant($activity),\n 'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),\n 'status' => $decorator->convertCallStatus($activity),\n 'durationMilliseconds' => (int) $activity->getDuration() * 1000,\n 'body' => $decorator->generateDescription($activity),\n ];\n\n // For now, this is just a \"disposition\" field to store Call Outcome.\n $metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n\n /**\n * @throws HubspotException\n * @throws BadRequest\n */\n private function buildTextMessagePayload(Activity $activity): Activity\n {\n $decorator = $this->getDecorateActivity();\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $decorator->formatCreatedAt($activity),\n 'type' => 'NOTE',\n ];\n\n $metadata = [\n 'body' => $decorator->generateDescription($activity),\n ];\n\n return $this->upsertActivity($activity, $engagement, $metadata);\n }\n\n private function getActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],\n 'ownerIds' => [],\n ];\n }\n\n /**\n * Get DecorateActivity instance. Can be overridden in tests for mocking.\n */\n protected function getDecorateActivity(): DecorateActivity\n {\n return new DecorateActivity();\n }\n\n public function saveFollowupActivity(Activity $activity, array $fields): ?string\n {\n if (empty($fields['subject'])) {\n return null;\n }\n\n $associations = $this->getActivityAssociations($activity);\n\n if (empty($fields['timestamp']) === false) {\n $formatTimestamp = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['timestamp']);\n $timestamp = $formatTimestamp->getTimestamp();\n } else {\n $timestamp = strtotime('+1 day');\n }\n\n $reminder = null;\n if (empty($fields['reminder']) === false) {\n $formatReminder = Carbon::createFromFormat('Y-m-d\\TH:i:s\\Z', $fields['reminder']);\n $reminder = $formatReminder->getTimestamp() * 1000;\n }\n\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->getCrmProviderId(),\n 'timestamp' => $timestamp * 1000,\n 'type' => 'TASK',\n ];\n\n $metadata = [\n 'subject' => $fields['subject'],\n 'body' => '',\n 'status' => 'NOT_STARTED',\n 'taskType' => 'CALL',\n 'reminders' => $reminder ? [$reminder] : [],\n ];\n\n try {\n $hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);\n } catch (BadRequest $e) {\n $this->logger->info('[HubSpot] Failed to create followup task', [\n 'activity' => $activity->getUuid(),\n 'engagement' => $engagement,\n 'metadata' => $metadata,\n 'error' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n // We don't actually create a corresponding activity object on our side yet.\n return (string) $hsEngagement->data->engagement->id;\n }\n\n private function logCrmEngagementManipulation(\n string $action,\n array $engagementData,\n array $data = [],\n array $associations = [],\n ): void {\n $payload = ['team' => $this->team->getUuid()];\n\n if ($action === self::ACTION_CREATE) {\n $payload['engagementData'] = $engagementData;\n $payload['data'] = $data;\n $payload['associations'] = $associations;\n }\n\n if ($action === self::ACTION_UPDATE) {\n $payload['engagementId'] = $engagementData['crmId'];\n $payload['data'] = $data;\n }\n\n if ($action === self::ACTION_DELETE) {\n $payload['engagementId'] = $engagementData['crmId'];\n }\n\n $this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);\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.85486114,"top":0.31,"width":0.018055556,"height":0.026666667},"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.87291664,"top":0.31,"width":0.018055556,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.9138889,"top":0.31,"width":0.059027776,"height":0.026666667},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"bounds":{"left":0.8645833,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.8854167,"top":0.34444445,"width":0.015277778,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.9048611,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.92569447,"top":0.34444445,"width":0.016666668,"height":0.02111111},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9458333,"top":0.3422222,"width":0.015277778,"height":0.025555555},"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.9611111,"top":0.3422222,"width":0.014583333,"height":0.025555555},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","depth":4,"on_screen":true,"value":"SELECT\n# DISTINCT\nopp.id as opp_id, opp.uuid, opp.name,\n# COUNT(dr.id) AS deal_risk_count,\n\n# dr.id,\n# cfd.value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\n# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id\n# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id\n\nLEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 1\nAND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\nGROUP BY opp.id, cfd.value\nORDER BY\n# cfd.value\n CAST(cfd.value AS UNSIGNED)\n# owner_name\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1\n# AND gdrt.is_enabled = 1)\nDESC\nLIMIT 25\n# OFFSET 0\n;\n\nselect * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';\nselect * from crm_layout_entities where crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4584045;\nSELECT * FROM crm_field_data WHERE object_id = 4584045;\n\nSELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'\n\nSELECT\n opp.id as opportunity_id,\n opp.name,\n COUNT(dr.id) as risk_count\nFROM opportunities opp\nLEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id\nWHERE opp.id IN (\n 6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788\n)\nGROUP BY opp.id;\n\nSELECT COUNT(dr.id)\n FROM deal_risks dr\n JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n WHERE dr.opportunity_id = 5563344\n# AND gdrt.group_id = usr.group_id\n\n\n\nEXPLAIN SELECT\n opp.id as opp_id, opp.uuid, opp.name,\n\ncfd.value,\ncfv.sequence,\ncfv.value,\n# MAX(cfd.value) AS max_cfd_value,\nusr.name AS owner_name,\nopp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,\nusr.uuid as owner_uuid,\nusr.photo_path as owner_photo,\nusr.id AS owner_id,\njt.name as owner_job,\nopp.stage_id, opp.stage_updated_at,\nacc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,\nrt.business_process_id AS pipeline_id\nFROM opportunities opp\n\nLEFT JOIN record_types rt ON opp.record_type_id = rt.id\nLEFT JOIN users usr ON opp.user_id = usr.id\nLEFT JOIN accounts acc ON opp.account_id = acc.id\nLEFT JOIN job_titles jt ON usr.job_title_id = jt.id\n\nLEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id\n FROM crm_field_data sub_cfd\n WHERE sub_cfd.object_id = opp.id\n AND sub_cfd.crm_field_id = 66810\n ORDER BY sub_cfd.updated_at DESC\n LIMIT 1)\n\nLEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value\n\nWHERE opp.user_id IS NOT NULL\nAND opp.deleted_at IS NULL\nAND opp.is_closed = 0\n# AND opp.is_closed = 1\n# AND opp.is_won = 0\nAND opp.close_date >= '2024-01-01 00:00:00'\nAND opp.close_date <= '2024-12-31 23:59:59'\nAND usr.team_id = 1\n\n# and opp.id = 4823179\n\n# GROUP BY opp.id\nORDER BY\n cfv.sequence DESC,\n# opp.name\n# owner_name\n cfd.value\n# CAST(cfd.value AS UNSIGNED)\n# CAST(MAX(cfd.value) AS UNSIGNED)\n# (SELECT COUNT(dr.id)\n# FROM deal_risks dr\n# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id\n# WHERE dr.opportunity_id = opp.id\n# AND dr.is_active = 1 AND gdrt.is_enabled = 1)\nDESC\n# LIMIT 75, 25\n;\n\n\n\nSELECT * FROM crm_field_values WHERE crm_field_id = 66810;\n\nSELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at\nFROM crm_fields f\nINNER JOIN crm_field_data fd ON fd.crm_field_id = f.id\n WHERE (f.crm_configuration_id = 1)\n AND (f.object_type = 'opportunity')\n# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))\n AND (fd.object_id IN (4158460))\n AND (f.crm_provider_id IN ('ForecastCategoryName'))\n ORDER BY fd.object_id ASC, fd.updated_at DESC;\n\nselect * from crm_layouts where crm_configuration_id = 1;\nselect cf.* from crm_fields cf\njoin crm_layout_entities cle on cf.id = cle.crm_field_id\nwhere crm_layout_id = 1493;\n\nSELECT * FROM opportunities WHERE id = 4158460;\nSELECT * FROM crm_field_data WHERE object_id = 4158460;\n\nselect * from users where team_id = 1;\nSELECT * FROM users WHERE id = 7160;\nselect * from role_user where user_id = 3248;\n\nselect * from crm_field_values where crm_field_id = 66824;\n\nSELECT * FROM users WHERE id = 23470;\nselect * from crm_configurations;\n\n# ******************************************\nselect * from teams where id = 1038; # 23521, 966\nselect * from users where team_id = 1038;\nselect * from crm_configurations where id = 966;\nSELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762\nSELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764\nSELECT * FROM participants WHERE activity_id = 54965764;\n\nSELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075\nSELECT * FROM participants WHERE activity_id = 54964075;\nselect f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id\n where tf.team_id = 1038;\n\n\n# ****************************************** PD *************************\nselect * from teams where id = 1029;\nSELECT * FROM crm_configurations WHERE id = 957;\nSELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421\n\n# ****************************************** Close *************************\nselect * from teams where id = 1031;\nSELECT * FROM crm_configurations WHERE id = 959;\nSELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517\n\n\nSELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433\n\nSELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009\n\n# ****************************************** SF *************************\nSELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;\n\nselect * from roles;\nselect * from permissions;\nselect * from permission_role where permission_id = 136;\n\nselect * from migrations order by id desc;\n\nselect * from teams where id IN (1, 1037);\nselect * from crm_layouts where crm_configuration_id = 1;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;\nSELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);\n\nselect * from features;\nselect * from team_features where feature_id = 33;\nselect * from opportunities;\n\nselect * from teams;\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 1052 and sa.provider = 'hubspot';\n\nSELECT * FROM accounts where id = 11512582;\nselect * from activities where account_id = 11512582;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.025,"top":0.06666667,"width":0.050694443,"height":0.034444444},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3382584868951134281
|
6551498163460912845
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12066 on JY-20725-handle Project: faVsco.js, menu
#12066 on JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
8
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Jiminny\Models\Activity;
use Jiminny\Models\Crm\Field;
use Jiminny\Repositories\Crm\FieldDataRepository;
use Jiminny\Services\Crm\Hubspot\DecorateActivity;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationCategoryV4;
use Jiminny\Services\Crm\Hubspot\Utils\AssociationTypeIdV4;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Http\Response;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Sentry;
trait WriteCrmTrait
{
private const string ACTION_CREATE = 'create';
private const string ACTION_UPDATE = 'update';
private const string ACTION_DELETE = 'delete';
/**
* @inheritdoc
*/
public function saveActivity(Activity $activity): Activity
{
switch ($activity->getCrmType()) {
case Activity::TYPE_CONFERENCE:
$activity = $this->buildConferencePayload($activity);
break;
case Activity::TYPE_SOFTPHONE:
case Activity::TYPE_SOFTPHONE_INBOUND:
$activity = $this->buildSoftphonePayload($activity);
break;
case Activity::TYPE_SMS_INBOUND:
case Activity::TYPE_SMS_OUTBOUND:
$activity = $this->buildTextMessagePayload($activity);
break;
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildConferencePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatStartTime($activity),
'type' => 'MEETING',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'meetingOutcome' => $decorator->convertMeetingOutcome($activity),
'internalMeetingNotes' => $decorator->generateDescription($activity),
];
// Only if they are creating a fresh engagement should we update the times, otherwise it will cause a cal sync.
if (! $activity->hasCrmProviderId()) {
if ($activity->hasActualStartTime() && $activity->hasActualEndTime()) {
$metadata['startTime'] = $decorator->formatStartTime($activity);
$metadata['endTime'] = $decorator->formatEndTime($activity);
}
$metadata['title'] = $decorator->generateTitle($activity);
}
// For now, this is just a "meetingOutcome" field for some customers.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function fetchCustomFieldData(Activity $activity, string $objectType): array
{
$payload = [];
$fieldDataRepository = app(FieldDataRepository::class);
$fieldData = $fieldDataRepository->getActivityFieldData($activity, $objectType);
foreach ($fieldData as $data) {
// Add the field and value to the payload.
$payload += [
$data->getField()->getCrmProviderId() => $data->getValue(),
];
}
return $payload;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function upsertActivity(Activity $activity, array $engagement, array $metadata): Activity
{
if (! $activity->hasCrmProviderId()) {
$this->createActivity($activity, $engagement, $metadata);
return $activity;
}
try {
$oldEngagement = $this->getClient()->getEngagementData($activity->getCrmProviderId());
} catch (HubspotException $e) {
$this->logger->warning('[HubSpot] Engagement not found', [
'activity_id' => $activity->getUuid(),
'crm_provider_id' => $activity->getCrmProviderId(),
'exception' => $e->getMessage(),
]);
// potentially here we can log new activity if it is not found
return $activity;
}
$type = $oldEngagement['engagement']['type'] ?? null;
$bodyAttribute = $activity->isTypeConference() ? 'internalMeetingNotes' : 'body';
$decorator = $this->getDecorateActivity();
if ($type !== 'TASK') {
$oldDescription = $oldEngagement['metadata'][$bodyAttribute] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldDescription
);
$oldAssociations = $oldEngagement['associations'] ?? [];
$this->updateNonTaskActivity($activity, $engagement, $metadata, $oldAssociations);
} else {
$oldTaskBody = $oldEngagement['metadata']['body'] ?? '';
$metadata[$bodyAttribute] = $decorator->mergeDescriptions(
$metadata[$bodyAttribute],
$oldTaskBody
);
$this->updateTask($activity, $engagement, $metadata);
}
return $activity;
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateNonTaskActivity(
Activity $activity,
array $engagement,
array $metadata,
array $oldAssociations
): void {
if ($activity->isTypeConference()) {
$this->updateMeetingV3($activity->getCrmProviderId(), $engagement, $metadata);
} else {
$this->getClient()->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);
}
$this->updateActivityAssociations($activity, $oldAssociations);
$this->logCrmEngagementManipulation(
self::ACTION_UPDATE,
['crmId' => $activity->getCrmProviderId()],
$metadata,
);
}
private function updateActivityAssociations(Activity $activity, array $oldAssociations): void
{
$currentAssociations = $this->getActivityAssociations($activity);
$associationChanges = $this->getAssociationDifferences($activity, $oldAssociations, $currentAssociations);
// No changes - exit
if (empty($associationChanges)) {
return;
}
$this->logger->info('[HubSpot] Updating associations', [
'team' => $this->team->getUuid(),
'activity' => $activity->getUuid(),
'associationChanges' => $associationChanges,
]);
$this->updateAssociations($activity, $associationChanges);
}
private function getAssociationDifferences(
Activity $activity,
array $oldAssociations,
array $currentAssociations
): array {
$objects = ['contactIds', 'companyIds', 'dealIds'];
$changes = [];
foreach ($objects as $object) {
$oldIds = $this->normalizeIds($oldAssociations[$object] ?? []);
$currentIds = $this->normalizeIds($currentAssociations[$object] ?? []);
$added = array_diff($currentIds, $oldIds);
$removed = array_diff($oldIds, $currentIds);
$type = match ($object) {
'contactIds' => 'contact',
'companyIds' => 'company',
'dealIds' => 'deal',
};
$shouldIncludeRemoved = $this->shouldIncludeRemovedAssociations($type, $added, $activity);
if (! empty($removed) && ! $shouldIncludeRemoved) {
$this->logger->info('[HubSpot] Skipping association removal', [
'team' => $this->team->getUuid(),
'type' => $type,
'oldIds' => $oldIds,
'currentIds' => $currentIds,
'skippedRemovals' => array_values($removed),
]);
}
if (! empty($added) || (! empty($removed) && $shouldIncludeRemoved)) {
$changes[$type] = [
'added' => array_values($added),
'removed' => array_values($removed),
];
}
}
return $changes;
}
/**
* Determines whether removed associations should be synced to HubSpot.
*
* For contacts and companies, we always sync removals (original behavior).
* For deals, we only sync removals when:
* - There are new deals being added (replacement scenario), OR
* - The activity has an account associated (indicating intentional association management)
*
* This prevents accidentally removing deal associations that exist in HubSpot
* but not in Jiminny's database (e.g., deals manually added by users in HubSpot).
*/
private function shouldIncludeRemovedAssociations(string $type, array $added, Activity $activity): bool
{
// Contacts and companies: always sync removals
if ($type !== 'deal') {
return true;
}
// Deals: sync removals if we're adding new deals (replacement)
if (! empty($added)) {
return true;
}
// Deals: sync removals only if activity has an account (intentional management)
return $activity->hasAccount();
}
private function normalizeIds(array $ids): array
{
$stringIds = array_map('strval', $ids);
$uniqueIds = array_unique($stringIds);
sort($uniqueIds);
return $uniqueIds;
}
private function updateAssociations(Activity $activity, array $associationChanges): void
{
$objectType = $activity->isTypeConference() ? 'meeting' : 'call';
foreach ($associationChanges as $type => $changes) {
if (! empty($changes['added'])) {
$associationType = (new AssociationTypeIdV4())->getAssociationType($objectType, $type);
$payload = $this->payloadBuilder->buildAddAssociationPayload(
$activity->getCrmProviderId(),
$changes['added'],
$associationType
);
$this->getClient()->addAssociations($objectType, $type, $payload);
}
if (! empty($changes['removed'])) {
$payload = $this->payloadBuilder->buildRemoveAssociationPayload(
$activity->getCrmProviderId(),
$changes['removed']
);
$this->getClient()->removeAssociations($objectType, $type, $payload);
}
}
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function updateMeetingV3(string $meetingId, array $engagement, array $metadata): Response
{
// If already set by us/client, do not override it
if (isset($engagement['activityType']) && $this->hasEngagementActivityType($meetingId)) {
$this->logger->info('[HubSpot] Meeting activity type already set', [
'meetingId' => $meetingId,
'activityType' => $engagement['activityType'],
]);
unset($engagement['activityType']);
}
$payload = $this->payloadBuilder->getV3MeetingPayload($engagement, $metadata);
return $this->getClient()->updateMeeting($meetingId, $payload);
}
private function hasEngagementActivityType(string $engagementId): bool
{
try {
$engagementData = $this->getClient()->getEngagementData($engagementId);
if (isset($engagementData['engagement']['activityType']) && trim($engagementData['engagement']['activityType']) != '') {
return true;
}
return false;
} catch (\Exception $exception) {
Sentry::captureException($exception);
$this->logger->info('[HubSpot] Exception checking engagement activity type, defaulting to preserve existing', [
'engagementId' => $engagementId,
'exception' => $exception->getMessage(),
]);
}
return true;
}
private function updateTask(Activity $activity, array $engagement, array $metadata): void
{
$this->getClient()->deleteEngagement($activity->getCrmProviderId());
$this->logCrmEngagementManipulation(
self::ACTION_DELETE,
['crmId' => $activity->getCrmProviderId()],
);
$this->createActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createActivity(Activity $activity, array $engagement, array $metadata): void
{
$associations = $this->getActivityAssociations($activity);
$associationsV4 = $this->convertActivityMeetingAssociationsV4($activity);
if ($activity->isTypeConference()) {
try {
$hsEngagement = $this->createMeetingV3($engagement, $associationsV4, $metadata);
$activityId = $hsEngagement->data->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associationsV4);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
} else {
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
$activityId = $hsEngagement->data->engagement->id;
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
$activity->crm_provider_id = $activityId;
$activity->save();
} catch (BadRequest $exception) {
Sentry::captureException($exception);
$this->logger->warning('[HubSpot] Error while creating engagements', [
'activity' => $activity->getUuid(),
'message' => $exception->getMessage(),
'engagement' => $engagement,
'associations' => $associations,
'metadata' => $metadata,
]);
}
}
}
private function convertActivityMeetingAssociationsV4(Activity $activity): array
{
$associations = [];
$contactIds = $this->getParticipantsIds($activity);
foreach ($contactIds as $contactId) {
$associations[] = $this->buildAssociation(
(int) $contactId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_CONTACT]
);
}
if ($activity->hasAccount()) {
$crmProviderId = $activity->getAccount()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_COMPANY]
);
}
}
if ($activity->hasOpportunity()) {
$crmProviderId = $activity->getOpportunity()?->getCrmProviderId();
if ($crmProviderId) {
$associations[] = $this->buildAssociation(
(int) $crmProviderId,
[AssociationCategoryV4::HUBSPOT_DEFINED => AssociationTypeIdV4::MEETING_TO_DEAL]
);
}
}
return $associations;
}
private function buildAssociation(int $id, array $categoryToTypeIds): array
{
$types = [];
foreach ($categoryToTypeIds as $category => $typeId) {
$types[] = $this->buildAssociationType($category, $typeId);
}
return [
'to' => ['id' => (string) $id],
'types' => $types,
];
}
private function buildAssociationType(string $category, int $typeId): array
{
return [
'associationCategory' => $category,
'associationTypeId' => $typeId,
];
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function createMeetingV3(array $engagement, array $associations, array $metadata): Response
{
$payload = $this->payloadBuilder->getV3MeetingPayload(
engagement: $engagement,
metadata: $metadata,
associations: $associations
);
return $this->getClient()->createMeeting($payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
private function buildSoftphonePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'CALL',
];
if ($activity->hasActivityType() && $this->matchesCrmType($activity->getActivityType())) {
$engagement['activityType'] = $activity->getActivityType()->getName();
}
$metadata = [
'toNumber' => $decorator->formatCallToParticipant($activity),
'fromNumber' => $activity->getFromParticipant()?->getPhoneNumber(),
'status' => $decorator->convertCallStatus($activity),
'durationMilliseconds' => (int) $activity->getDuration() * 1000,
'body' => $decorator->generateDescription($activity),
];
// For now, this is just a "disposition" field to store Call Outcome.
$metadata = array_merge($metadata, $this->fetchCustomFieldData($activity, Field::OBJECT_TASK));
return $this->upsertActivity($activity, $engagement, $metadata);
}
/**
* @throws HubspotException
* @throws BadRequest
*/
private function buildTextMessagePayload(Activity $activity): Activity
{
$decorator = $this->getDecorateActivity();
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $decorator->formatCreatedAt($activity),
'type' => 'NOTE',
];
$metadata = [
'body' => $decorator->generateDescription($activity),
];
return $this->upsertActivity($activity, $engagement, $metadata);
}
private function getActivityAssociations(Activity $activity): array
{
return [
'contactIds' => $this->getParticipantsIds($activity),
'companyIds' => $activity->hasAccount() ? [$activity->getAccount()->getCrmProviderId()] : [],
'dealIds' => $activity->hasOpportunity() ? [$activity->getOpportunity()->getCrmProviderId()] : [],
'ownerIds' => [],
];
}
/**
* Get DecorateActivity instance. Can be overridden in tests for mocking.
*/
protected function getDecorateActivity(): DecorateActivity
{
return new DecorateActivity();
}
public function saveFollowupActivity(Activity $activity, array $fields): ?string
{
if (empty($fields['subject'])) {
return null;
}
$associations = $this->getActivityAssociations($activity);
if (empty($fields['timestamp']) === false) {
$formatTimestamp = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['timestamp']);
$timestamp = $formatTimestamp->getTimestamp();
} else {
$timestamp = strtotime('+1 day');
}
$reminder = null;
if (empty($fields['reminder']) === false) {
$formatReminder = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $fields['reminder']);
$reminder = $formatReminder->getTimestamp() * 1000;
}
$engagement = [
'active' => true,
'ownerId' => $this->profile->getCrmProviderId(),
'timestamp' => $timestamp * 1000,
'type' => 'TASK',
];
$metadata = [
'subject' => $fields['subject'],
'body' => '',
'status' => 'NOT_STARTED',
'taskType' => 'CALL',
'reminders' => $reminder ? [$reminder] : [],
];
try {
$hsEngagement = $this->getClient()->createEngagement($engagement, $associations, $metadata);
} catch (BadRequest $e) {
$this->logger->info('[HubSpot] Failed to create followup task', [
'activity' => $activity->getUuid(),
'engagement' => $engagement,
'metadata' => $metadata,
'error' => $e->getMessage(),
]);
return null;
}
$this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);
// We don't actually create a corresponding activity object on our side yet.
return (string) $hsEngagement->data->engagement->id;
}
private function logCrmEngagementManipulation(
string $action,
array $engagementData,
array $data = [],
array $associations = [],
): void {
$payload = ['team' => $this->team->getUuid()];
if ($action === self::ACTION_CREATE) {
$payload['engagementData'] = $engagementData;
$payload['data'] = $data;
$payload['associations'] = $associations;
}
if ($action === self::ACTION_UPDATE) {
$payload['engagementId'] = $engagementData['crmId'];
$payload['data'] = $data;
}
if ($action === self::ACTION_DELETE) {
$payload['engagementId'] = $engagementData['crmId'];
}
$this->logger->info('[HubSpot] Engagement manipulated (' . $action . ')', $payload);
}
}
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
8
1
3
4
Previous Highlighted Error
Next Highlighted Error
SELECT
# DISTINCT
opp.id as opp_id, opp.uuid, opp.name,
# COUNT(dr.id) AS deal_risk_count,
# dr.id,
# cfd.value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
# LEFT JOIN group_deal_risk_types gdrt ON gdrt.group_id = usr.group_id
# LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id AND dr.group_deal_risk_type_id = gdrt.id
LEFT JOIN crm_field_data cfd ON (cfd.object_id = opp.id and cfd.crm_field_id = 66814)
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 1
AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
GROUP BY opp.id, cfd.value
ORDER BY
# cfd.value
CAST(cfd.value AS UNSIGNED)
# owner_name
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1
# AND gdrt.is_enabled = 1)
DESC
LIMIT 25
# OFFSET 0
;
select * from crm_fields where crm_configuration_id = 1 and object_type = 'opportunity';
select * from crm_layout_entities where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4584045;
SELECT * FROM crm_field_data WHERE object_id = 4584045;
SELECT * FROM opportunities where team_id = 1 and crm_provider_id = '0061K00000lfb9IQAQ'
SELECT
opp.id as opportunity_id,
opp.name,
COUNT(dr.id) as risk_count
FROM opportunities opp
LEFT JOIN deal_risks dr ON dr.opportunity_id = opp.id
WHERE opp.id IN (
6069574,8900938,9507638,9524799,9660490,9662824,9705675,9749758,4158460,4391812,6450439,4448422,5118945,5590675,4584045,7228149,9002408,9165534,9446720,9641778,9665149,9703344,9709280,5747948,4158491,6182565,6263970,5798120,5111315,4536978,6062352,6548383,6072095,6548225,4480986,5011422,6548381,8760540,8917554,9509986,9514392,9569009,9569011,9578490,9713604,5784397,7276612,7288405,8994421,9118219,9608148,9818911,4510535,6479693,5958049,6271674,6550448,4158331,4158483,6126571,6171615,6540943,4897466,5190896,5796182,5932762,8572433,8723698,8892697,9711001,9789264,4549188,6100831,6170064,6260799,6263653,6449936,6530871,6538978,7777651,8059269,8319918,8787049,8901150,9263153,9453207,9514738,9696700,9791062,5752018,6421452,7439134,7878923,9354763,9369285,9514396,9582506,9889949,9890216,4094311,4158495,4158496,6098128,5585661,3872564,6442149,5891604,6164746,6199593,6583474,6519684,9018490,9809006,4496897,5041324,5829430,6198319,6253504,6555763,7242914,7931055,8024125,8797814,8058559,8673347,8892695,8994420,9616219,9714970,9722004,9809439,9818918,6523177,8134147,9002915,9711422,9892713,9901719,9954210,9978435,5800810,6243518,6416114,6222251,6411974,6512456,5791953,6545606,9914780,5805540,6238986,6463838,6547680,9767049,9809437,9810885,9890855,6673493,9902036,4335521,6379871,6503799,6546077,8018765,9907556,9958433,9905855,9916179,9946741,9957877,5563344,6271838,6450815,7641128,7762567,7780592,8684810,8685786,8685787,8685788
)
GROUP BY opp.id;
SELECT COUNT(dr.id)
FROM deal_risks dr
JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
WHERE dr.opportunity_id = 5563344
# AND gdrt.group_id = usr.group_id
EXPLAIN SELECT
opp.id as opp_id, opp.uuid, opp.name,
cfd.value,
cfv.sequence,
cfv.value,
# MAX(cfd.value) AS max_cfd_value,
usr.name AS owner_name,
opp.value, opp.currency_code, opp.close_date, opp.remotely_created_at, opp.is_closed, opp.is_won,
usr.uuid as owner_uuid,
usr.photo_path as owner_photo,
usr.id AS owner_id,
jt.name as owner_job,
opp.stage_id, opp.stage_updated_at,
acc.name AS acc_name, opp.stage_updated_at, acc.crm_provider_id AS acc_provider_id, opp.crm_provider_id AS opp_provider_id,
rt.business_process_id AS pipeline_id
FROM opportunities opp
LEFT JOIN record_types rt ON opp.record_type_id = rt.id
LEFT JOIN users usr ON opp.user_id = usr.id
LEFT JOIN accounts acc ON opp.account_id = acc.id
LEFT JOIN job_titles jt ON usr.job_title_id = jt.id
LEFT JOIN crm_field_data cfd ON cfd.id = (SELECT sub_cfd.id
FROM crm_field_data sub_cfd
WHERE sub_cfd.object_id = opp.id
AND sub_cfd.crm_field_id = 66810
ORDER BY sub_cfd.updated_at DESC
LIMIT 1)
LEFT JOIN crm_field_values cfv ON cfv.crm_field_id = 66810 AND cfv.value = cfd.value
WHERE opp.user_id IS NOT NULL
AND opp.deleted_at IS NULL
AND opp.is_closed = 0
# AND opp.is_closed = 1
# AND opp.is_won = 0
AND opp.close_date >= '2024-01-01 00:00:00'
AND opp.close_date <= '2024-12-31 23:59:59'
AND usr.team_id = 1
# and opp.id = 4823179
# GROUP BY opp.id
ORDER BY
cfv.sequence DESC,
# opp.name
# owner_name
cfd.value
# CAST(cfd.value AS UNSIGNED)
# CAST(MAX(cfd.value) AS UNSIGNED)
# (SELECT COUNT(dr.id)
# FROM deal_risks dr
# JOIN group_deal_risk_types gdrt ON dr.group_deal_risk_type_id = gdrt.id
# WHERE dr.opportunity_id = opp.id
# AND dr.is_active = 1 AND gdrt.is_enabled = 1)
DESC
# LIMIT 75, 25
;
SELECT * FROM crm_field_values WHERE crm_field_id = 66810;
SELECT f.id, f.crm_provider_id AS field_name, f.label, fd.object_id AS dealId, fd.value, fd.updated_at, fd.id, fd.created_at
FROM crm_fields f
INNER JOIN crm_field_data fd ON fd.crm_field_id = f.id
WHERE (f.crm_configuration_id = 1)
AND (f.object_type = 'opportunity')
# AND (fd.object_id IN (4158331,4158483,4158495,4158496,6069574,6263970,6171615,6540943,6545606,5829430,6198319,6263653,6548381,7242914,8797814,7228149,7439134,7878923,8134147,9002915,9118219,9165534,9354763,9369285,9446720))
AND (fd.object_id IN (4158460))
AND (f.crm_provider_id IN ('ForecastCategoryName'))
ORDER BY fd.object_id ASC, fd.updated_at DESC;
select * from crm_layouts where crm_configuration_id = 1;
select cf.* from crm_fields cf
join crm_layout_entities cle on cf.id = cle.crm_field_id
where crm_layout_id = 1493;
SELECT * FROM opportunities WHERE id = 4158460;
SELECT * FROM crm_field_data WHERE object_id = 4158460;
select * from users where team_id = 1;
SELECT * FROM users WHERE id = 7160;
select * from role_user where user_id = 3248;
select * from crm_field_values where crm_field_id = 66824;
SELECT * FROM users WHERE id = 23470;
select * from crm_configurations;
# [PASSWORD_DOTS]
select * from teams where id = 1038; # 23521, 966
select * from users where team_id = 1038;
select * from crm_configurations where id = 966;
SELECT * FROM activities WHERE uuid_to_bin('e8dc7c34-31f6-4430-bd81-2d9d8f00dd07') = uuid; # 54965762
SELECT * FROM activities WHERE uuid_to_bin('4b5727c0-69d1-428f-b8d9-b649055166e2') = uuid; # 54965764
SELECT * FROM participants WHERE activity_id = 54965764;
SELECT id, uuid, title, recording_state FROM activities WHERE uuid_to_bin('355f105d-5606-4379-b0c5-d91daf59f4ce') = uuid; # 54964075
SELECT * FROM participants WHERE activity_id = 54964075;
select f.id, slug, title, tf.team_id from team_features tf JOIN features f on tf.feature_id = f.id
where tf.team_id = 1038;
# [PASSWORD_DOTS] PD [PASSWORD_DOTS]
select * from teams where id = 1029;
SELECT * FROM crm_configurations WHERE id = 957;
SELECT * FROM activities WHERE uuid_to_bin('581a33cb-4343-44bd-ac0c-4a5cee71c5ec') = uuid; # 54963423
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('8b6bb2f7-e7a7-4aec-8806-fbfea7093ccd') = uuid; # 54963421
# [PASSWORD_DOTS] Close [PASSWORD_DOTS]
select * from teams where id = 1031;
SELECT * FROM crm_configurations WHERE id = 959;
SELECT * FROM activities WHERE uuid_to_bin('59c517e0-96c9-4dad-bfeb-b7bef72f8725') = uuid; # 54963517
SELECT * FROM activities WHERE uuid_to_bin('07238011-25fa-418e-838b-fb21e82b9ea2') = uuid; # 54963433
SELECT * FROM activities WHERE uuid_to_bin('d68311e3-ac34-49bd-bf04-88323a7e5352') = uuid; # 54966009
# [PASSWORD_DOTS] SF [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('2bd4cfa3-4eb5-4c77-84a7-9ed46a21c988') = uuid;
select * from roles;
select * from permissions;
select * from permission_role where permission_id = 136;
select * from migrations order by id desc;
select * from teams where id IN (1, 1037);
select * from crm_layouts where crm_configuration_id = 1;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 1493;
SELECT * FROM crm_fields WHERE id IN (1652,1661,66799,66814,66821,66836,66843,66846,66864,84752,182306,323580,380378);
select * from features;
select * from team_features where feature_id = 33;
select * from opportunities;
select * from teams;
SELECT
CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,
u.email,
sa.*,
t.owner_id FROM social_accounts sa
JOIN users u on u.id = sa.sociable_id
JOIN teams t on t.id = u.team_id
WHERE u.team_id = 1052 and sa.provider = 'hubspot';
SELECT * FROM accounts where id = 11512582;
select * from activities where account_id = 11512582;
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|